@effect-agent/pr-review 0.1.0-beta.28 → 0.1.0-beta.29

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.
Files changed (52) hide show
  1. package/README.md +9 -204
  2. package/dist/index.d.mts +87 -914
  3. package/dist/index.mjs +163 -71
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +3 -18
  6. package/src/index.ts +1 -25
  7. package/src/review.ts +212 -0
  8. package/dist/action.d.mts +0 -215
  9. package/dist/action.mjs +0 -505
  10. package/dist/action.mjs.map +0 -1
  11. package/dist/cli.d.mts +0 -1
  12. package/dist/cli.mjs +0 -106
  13. package/dist/cli.mjs.map +0 -1
  14. package/dist/fan-out-C3yG1cx3.d.mts +0 -1526
  15. package/dist/github-CCuLgyqb.mjs +0 -3437
  16. package/dist/github-CCuLgyqb.mjs.map +0 -1
  17. package/dist/logging-Q4j0oub-.mjs +0 -75
  18. package/dist/logging-Q4j0oub-.mjs.map +0 -1
  19. package/dist/providers-Br9FRn7j.mjs +0 -1349
  20. package/dist/providers-Br9FRn7j.mjs.map +0 -1
  21. package/dist/testing.d.mts +0 -86
  22. package/dist/testing.mjs +0 -184
  23. package/dist/testing.mjs.map +0 -1
  24. package/src/action.ts +0 -906
  25. package/src/cli.ts +0 -235
  26. package/src/internal/action-entry.ts +0 -45
  27. package/src/internal/adjudication.ts +0 -415
  28. package/src/internal/anchors.ts +0 -20
  29. package/src/internal/coverage.ts +0 -357
  30. package/src/internal/diff.ts +0 -193
  31. package/src/internal/effort.ts +0 -86
  32. package/src/internal/factory.ts +0 -357
  33. package/src/internal/fan-out-scripted.ts +0 -77
  34. package/src/internal/fan-out.ts +0 -1148
  35. package/src/internal/fingerprint.ts +0 -89
  36. package/src/internal/fixtures.ts +0 -148
  37. package/src/internal/github-env.ts +0 -164
  38. package/src/internal/github.ts +0 -1218
  39. package/src/internal/ignore.ts +0 -88
  40. package/src/internal/logging.ts +0 -124
  41. package/src/internal/profiles.ts +0 -91
  42. package/src/internal/progress.ts +0 -433
  43. package/src/internal/providers.ts +0 -133
  44. package/src/internal/render.ts +0 -819
  45. package/src/internal/retirement.ts +0 -337
  46. package/src/internal/review-agent.ts +0 -543
  47. package/src/internal/review-state.ts +0 -782
  48. package/src/internal/review-units.ts +0 -493
  49. package/src/internal/run.ts +0 -611
  50. package/src/internal/scripted.ts +0 -108
  51. package/src/internal/source.ts +0 -110
  52. package/src/testing.ts +0 -8
@@ -1,493 +0,0 @@
1
- import { Schema } from "effect";
2
-
3
- import type { ChangedFile } from "./diff.ts";
4
- import { ChangedPath, isReviewableFile } from "./diff.ts";
5
- import {
6
- fileReviewEvidenceChunks,
7
- type FindingSeverity,
8
- MAX_PATCH_CHARS,
9
- type ReviewConcern,
10
- type ReviewFinding,
11
- } from "./review-agent.ts";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Pure, deterministic planning for the fan-out reviewer: group the changeset
15
- // into bounded review units (the work one delegated child reviews) and merge
16
- // the children's findings back into one bounded review. Both operations are
17
- // plain functions so tests pin them directly and the coordinator's tool
18
- // surface stays deterministic — grouping is an algorithm, not model prose.
19
- // ---------------------------------------------------------------------------
20
-
21
- /** The delegation fan-out bound: one parent Run spawns at most this many children. */
22
- export const MAX_REVIEW_UNITS = 8;
23
-
24
- /** A unit never carries more files than this, regardless of their size. */
25
- export const MAX_UNIT_FILES = 12;
26
-
27
- /**
28
- * Bound the complete model-visible evidence assigned to one child. This is a
29
- * character bound rather than a token estimate because it is deterministic,
30
- * provider-independent, and enforced before any model call.
31
- */
32
- export const UNIT_EVIDENCE_CHAR_BUDGET = 240_000;
33
-
34
- /** Maximum complete evidence shards placed in one child brief. */
35
- export const MAX_UNIT_EVIDENCE_SHARDS = 12;
36
-
37
- /**
38
- * Keep overflow diagnostics bounded to one plan's total assignment capacity.
39
- * The plan separately records the exact overflow count and every affected
40
- * path, so identifiers are a deterministic diagnostic sample rather than the
41
- * authority for whether input coverage is complete.
42
- */
43
- export const MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS = MAX_REVIEW_UNITS * MAX_UNIT_EVIDENCE_SHARDS;
44
-
45
- /** The merged review never exceeds the `CodeReview` findings bound. */
46
- export const MAX_MERGED_FINDINGS = 20;
47
-
48
- export const ReviewUnitId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
49
-
50
- /** High-risk surfaces that receive an explicit specialist focus label. */
51
- export const ReviewRiskCategory = Schema.Literals([
52
- "authentication-authorization",
53
- "security-boundary",
54
- "persistence-durability",
55
- "concurrency",
56
- "credential-handling",
57
- "external-side-effects",
58
- ]);
59
- export type ReviewRiskCategory = typeof ReviewRiskCategory.Type;
60
-
61
- export const ReviewDiscoveryPerspective = Schema.Literals(["general", "risk-specialist"]);
62
- export type ReviewDiscoveryPerspective = typeof ReviewDiscoveryPerspective.Type;
63
-
64
- export const ReviewPassId = Schema.NonEmptyString.check(Schema.isMaxLength(64));
65
- export const ReviewEvidenceShardId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
66
-
67
- /** One complete bounded slice of a changed path's model-visible evidence. */
68
- export class ReviewEvidenceShard extends Schema.Class<ReviewEvidenceShard>(
69
- "@effect-agent/pr-review/ReviewEvidenceShard",
70
- )({
71
- shardId: ReviewEvidenceShardId,
72
- path: ChangedPath,
73
- ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
74
- total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
75
- evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(
76
- Schema.isLessThanOrEqualTo(MAX_PATCH_CHARS),
77
- ),
78
- }) {}
79
-
80
- const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId)
81
- .check(Schema.isMinLength(1))
82
- .check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS));
83
-
84
- /** One required, independently scoped discovery attempt. */
85
- export class ReviewDiscoveryPass extends Schema.Class<ReviewDiscoveryPass>(
86
- "@effect-agent/pr-review/ReviewDiscoveryPass",
87
- )({
88
- passId: ReviewPassId,
89
- unitId: ReviewUnitId,
90
- paths: Schema.Array(ChangedPath)
91
- .check(Schema.isMinLength(1))
92
- .check(Schema.isMaxLength(MAX_UNIT_FILES)),
93
- evidenceShardIds: EvidenceShardIds,
94
- perspective: ReviewDiscoveryPerspective,
95
- /** Empty for the general pass; explicit deterministic focus for specialists. */
96
- riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6)),
97
- }) {}
98
-
99
- /** One bounded slice of the changeset delegated to one child reviewer. */
100
- export class ReviewUnit extends Schema.Class<ReviewUnit>("@effect-agent/pr-review/ReviewUnit")({
101
- unitId: ReviewUnitId,
102
- paths: Schema.Array(ChangedPath)
103
- .check(Schema.isMinLength(1))
104
- .check(Schema.isMaxLength(MAX_UNIT_FILES)),
105
- evidenceShards: Schema.Array(ReviewEvidenceShard)
106
- .check(Schema.isMinLength(1))
107
- .check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS)),
108
- /** additions + deletions across the unit's files, for honest sizing. */
109
- changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
110
- /** Complete model-visible diff/content evidence assigned to each child. */
111
- evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(
112
- Schema.isLessThanOrEqualTo(UNIT_EVIDENCE_CHAR_BUDGET),
113
- ),
114
- /** Host-classified focus labels for the unit's redundant specialist pass. */
115
- riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6)),
116
- }) {}
117
-
118
- /** The complete deterministic fan-out plan over one changeset. */
119
- export class ReviewUnitPlan extends Schema.Class<ReviewUnitPlan>(
120
- "@effect-agent/pr-review/ReviewUnitPlan",
121
- )({
122
- totalFiles: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
123
- /** True when the source returned fewer files than the pull request has. */
124
- truncated: Schema.Boolean,
125
- units: Schema.Array(ReviewUnit).check(Schema.isMaxLength(MAX_REVIEW_UNITS)),
126
- /** Exact discovery calls the coordinator must make. */
127
- discoveryPasses: Schema.Array(ReviewDiscoveryPass).check(
128
- Schema.isMaxLength(MAX_REVIEW_UNITS * 2),
129
- ),
130
- /** Changed files with neither a textual diff nor bounded base/head text. */
131
- undiffablePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
132
- /** Assigned paths with one or more evidence shards beyond plan capacity. */
133
- partialEvidencePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
134
- /** Exact number of shards beyond the bounded unit capacity. */
135
- unassignedEvidenceShardCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
136
- /** Bounded deterministic prefix of the unassigned shard identifiers. */
137
- unassignedEvidenceShardIds: Schema.Array(ReviewEvidenceShardId).check(
138
- Schema.isMaxLength(MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS),
139
- ),
140
- /**
141
- * Diffable files beyond the fan-out capacity (MAX_REVIEW_UNITS units of
142
- * MAX_UNIT_FILES files). Never silently dropped: the coordinator must name
143
- * them as unreviewed in its summary.
144
- */
145
- unassignedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
146
- }) {}
147
-
148
- const riskRules: ReadonlyArray<{
149
- readonly category: ReviewRiskCategory;
150
- readonly patterns: ReadonlyArray<RegExp>;
151
- }> = [
152
- {
153
- category: "authentication-authorization",
154
- patterns: [/auth/, /authoriz/, /permission/, /principal/, /access[-_ ]?control/, /role\b/],
155
- },
156
- {
157
- category: "security-boundary",
158
- patterns: [
159
- /security/,
160
- /sandbox/,
161
- /untrusted/,
162
- /schema\.decode/,
163
- /validation/,
164
- /injection/,
165
- /csrf/,
166
- /xss/,
167
- /path traversal/,
168
- ],
169
- },
170
- {
171
- category: "persistence-durability",
172
- patterns: [
173
- /durab/,
174
- /persist/,
175
- /storage/,
176
- /database/,
177
- /\bsql\b/,
178
- /journal/,
179
- /ledger/,
180
- /checkpoint/,
181
- /migration/,
182
- /transaction/,
183
- ],
184
- },
185
- {
186
- category: "concurrency",
187
- patterns: [
188
- /concurr/,
189
- /semaphore/,
190
- /\bfiber/,
191
- /race/,
192
- /mutex/,
193
- /\block\b/,
194
- /queue/,
195
- /parallel/,
196
- /interrupt/,
197
- ],
198
- },
199
- {
200
- category: "credential-handling",
201
- patterns: [/credential/, /secret/, /password/, /api[-_ ]?key/, /bearer/, /hmac/, /signature/],
202
- },
203
- {
204
- category: "external-side-effects",
205
- patterns: [
206
- /publish/,
207
- /webhook/,
208
- /github/,
209
- /fetch\(/,
210
- /http/,
211
- /send[-_ ]?(email|message)/,
212
- /write[-_ ]?(file|record)/,
213
- /delete/,
214
- /mutation/,
215
- /side[-_ ]?effect/,
216
- /spawn/,
217
- /exec/,
218
- ],
219
- },
220
- ];
221
-
222
- /**
223
- * Deterministic host policy for specialist assignment. It intentionally
224
- * favors false positives: an extra bounded pass costs work, while a missed
225
- * high-risk classification removes redundancy. This is not a claim that the
226
- * keyword policy recognizes every semantically risky change.
227
- */
228
- export const classifyReviewRisks = (file: ChangedFile): ReadonlyArray<ReviewRiskCategory> => {
229
- const text = [
230
- file.path,
231
- file.previousPath ?? "",
232
- file.patch ?? "",
233
- file.reviewBaseContent ?? "",
234
- file.reviewHeadContent ?? "",
235
- ]
236
- .join("\n")
237
- .toLowerCase();
238
- return riskRules
239
- .filter((rule) => rule.patterns.some((pattern) => pattern.test(text)))
240
- .map((rule) => rule.category);
241
- };
242
-
243
- /**
244
- * Whether every claimed finding anchor was present in the exact bounded
245
- * evidence shards assigned to one unit. This is stricter than checking the
246
- * full pull-request diff when an oversized path spans multiple units.
247
- */
248
- export const findingAnchorInUnitEvidence = (
249
- finding: ReviewFinding,
250
- unit: ReviewUnit,
251
- files: ReadonlyArray<ChangedFile>,
252
- ): boolean => {
253
- const file = files.find((candidate) => candidate.path === finding.path);
254
- if (file?.patch === undefined || finding.endLine < finding.startLine) return false;
255
- const assignedOrdinals = new Set(
256
- unit.evidenceShards
257
- .filter((shard) => shard.path === finding.path)
258
- .map((shard) => shard.ordinal),
259
- );
260
- const visibleLines = new Set<number>();
261
- const chunks = fileReviewEvidenceChunks(file);
262
- for (let index = 0; index < chunks.length; index += 1) {
263
- if (!assignedOrdinals.has(index + 1)) continue;
264
- for (const line of chunks[index]?.annotatedPatch.split("\n") ?? []) {
265
- const match = /^R(\d+) /.exec(line);
266
- if (match?.[1] !== undefined) visibleLines.add(Number(match[1]));
267
- }
268
- }
269
- for (let line = finding.startLine; line <= finding.endLine; line += 1) {
270
- if (!visibleLines.has(line)) return false;
271
- }
272
- return true;
273
- };
274
-
275
- interface PlannedEvidenceShard {
276
- readonly shard: ReviewEvidenceShard;
277
- readonly file: ChangedFile;
278
- readonly changedLines: number;
279
- }
280
-
281
- const uniquePaths = (shards: ReadonlyArray<PlannedEvidenceShard>): ReadonlyArray<string> => [
282
- ...new Set(shards.map(({ shard }) => shard.path)),
283
- ];
284
-
285
- const plannedEvidenceShards = (
286
- files: ReadonlyArray<ChangedFile>,
287
- ): ReadonlyArray<PlannedEvidenceShard> => {
288
- const planned: Array<PlannedEvidenceShard> = [];
289
- let shardIndex = 0;
290
- for (const file of files) {
291
- const chunks = fileReviewEvidenceChunks(file);
292
- for (let index = 0; index < chunks.length; index += 1) {
293
- const chunk = chunks[index];
294
- if (chunk === undefined) continue;
295
- shardIndex += 1;
296
- planned.push({
297
- shard: ReviewEvidenceShard.make({
298
- shardId: `shard-${String(shardIndex).padStart(4, "0")}`,
299
- path: file.path,
300
- ordinal: index + 1,
301
- total: chunks.length,
302
- evidenceChars: chunk.annotatedPatch.length,
303
- }),
304
- file,
305
- changedLines: index === 0 ? file.additions + file.deletions : 0,
306
- });
307
- }
308
- }
309
- return planned;
310
- };
311
-
312
- const unitOf = (index: number, shards: ReadonlyArray<PlannedEvidenceShard>): ReviewUnit =>
313
- ReviewUnit.make({
314
- unitId: `unit-${String(index + 1).padStart(3, "0")}`,
315
- paths: uniquePaths(shards),
316
- evidenceShards: shards.map(({ shard }) => shard),
317
- changedLines: shards.reduce((total, shard) => total + shard.changedLines, 0),
318
- evidenceChars: shards.reduce((total, { shard }) => total + shard.evidenceChars, 0),
319
- riskCategories: [...new Set(shards.flatMap(({ file }) => classifyReviewRisks(file)))],
320
- });
321
-
322
- const discoveryPassesFor = (units: ReadonlyArray<ReviewUnit>): ReadonlyArray<ReviewDiscoveryPass> =>
323
- units.flatMap((unit) => [
324
- ReviewDiscoveryPass.make({
325
- passId: `${unit.unitId}-general`,
326
- unitId: unit.unitId,
327
- paths: unit.paths,
328
- evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
329
- perspective: "general",
330
- riskCategories: [],
331
- }),
332
- ReviewDiscoveryPass.make({
333
- passId: `${unit.unitId}-specialist`,
334
- unitId: unit.unitId,
335
- paths: unit.paths,
336
- evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
337
- perspective: "risk-specialist",
338
- riskCategories: unit.riskCategories,
339
- }),
340
- ]);
341
-
342
- /**
343
- * Group the changeset into at most `MAX_REVIEW_UNITS` review units.
344
- *
345
- * Deterministic by construction: files are ordered by path (so files sharing
346
- * a directory become neighbors — directory affinity without a heuristic),
347
- * then split into complete line-bounded evidence shards and packed greedily
348
- * under the hard evidence and per-unit shard bounds. Capacity is finite and
349
- * explicit:
350
- *
351
- * - files without a textual diff are still delegated when the source
352
- * recovered complete bounded UTF-8 base/head content. Findings from that
353
- * evidence cannot anchor inline and are reported as concerns;
354
- * - files with neither form of textual evidence surface in
355
- * `undiffablePaths` instead of laundering missing coverage;
356
- * - an oversized path spans as many deterministic shards and units as needed;
357
- * - shards beyond `MAX_REVIEW_UNITS` full units surface explicitly, so a path
358
- * is partial only when finite plan capacity is genuinely exhausted.
359
- */
360
- export const planReviewUnits = (
361
- files: ReadonlyArray<ChangedFile>,
362
- options: { readonly totalChangedFiles: number },
363
- ): ReviewUnitPlan => {
364
- const ordered = [...files].sort((left, right) => (left.path < right.path ? -1 : 1));
365
- const reviewable = ordered.filter(isReviewableFile);
366
- const undiffable = ordered.filter((file) => !isReviewableFile(file));
367
-
368
- const shards = plannedEvidenceShards(reviewable);
369
- const groups: Array<Array<PlannedEvidenceShard>> = [];
370
- const unassigned: Array<PlannedEvidenceShard> = [];
371
- let current: Array<PlannedEvidenceShard> = [];
372
- let currentEvidenceChars = 0;
373
- for (const shard of shards) {
374
- const nextPaths = new Set([...uniquePaths(current), shard.shard.path]);
375
- const wouldOverflow =
376
- current.length >= MAX_UNIT_EVIDENCE_SHARDS ||
377
- nextPaths.size > MAX_UNIT_FILES ||
378
- (current.length > 0 &&
379
- currentEvidenceChars + shard.shard.evidenceChars > UNIT_EVIDENCE_CHAR_BUDGET);
380
- if (wouldOverflow) {
381
- groups.push(current);
382
- current = [];
383
- currentEvidenceChars = 0;
384
- }
385
- if (groups.length >= MAX_REVIEW_UNITS) {
386
- unassigned.push(shard);
387
- continue;
388
- }
389
- current.push(shard);
390
- currentEvidenceChars += shard.shard.evidenceChars;
391
- }
392
- if (current.length > 0 && groups.length < MAX_REVIEW_UNITS) {
393
- groups.push(current);
394
- }
395
-
396
- const units = groups.map((group, index) => unitOf(index, group));
397
- const assignedShardIds = new Set(
398
- units.flatMap((unit) => unit.evidenceShards.map((shard) => shard.shardId)),
399
- );
400
- const assignedPaths = new Set(
401
- shards
402
- .filter(({ shard }) => assignedShardIds.has(shard.shardId))
403
- .map(({ shard }) => shard.path),
404
- );
405
- const unassignedPathsWithEvidence = new Set(unassigned.map(({ shard }) => shard.path));
406
- return ReviewUnitPlan.make({
407
- totalFiles: files.length,
408
- truncated: files.length < options.totalChangedFiles,
409
- units,
410
- discoveryPasses: discoveryPassesFor(units),
411
- undiffablePaths: undiffable.map((file) => file.path),
412
- partialEvidencePaths: [...unassignedPathsWithEvidence].filter((path) =>
413
- assignedPaths.has(path),
414
- ),
415
- unassignedEvidenceShardCount: unassigned.length,
416
- unassignedEvidenceShardIds: unassigned
417
- .slice(0, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS)
418
- .map(({ shard }) => shard.shardId),
419
- unassignedPaths: [...unassignedPathsWithEvidence].filter((path) => !assignedPaths.has(path)),
420
- });
421
- };
422
-
423
- const severityRank: Record<FindingSeverity, number> = {
424
- blocking: 0,
425
- important: 1,
426
- nit: 2,
427
- };
428
-
429
- const anchorKey = (finding: ReviewFinding): string =>
430
- `${finding.path} ${finding.startLine} ${finding.endLine}`;
431
-
432
- /**
433
- * Merge the children's findings into one bounded, deterministic list: dedupe
434
- * findings sharing an anchor (path + line range) keeping the most severe —
435
- * and, at equal severity, the first in declaration order — then rank by
436
- * severity, path, and line, and cap at the `CodeReview` findings bound.
437
- * This is the merge policy the coordinator's instructions state in prose;
438
- * pinning it here keeps the policy itself deterministic and testable.
439
- */
440
- export const rankAndDedupeFindings = (
441
- findings: ReadonlyArray<ReviewFinding>,
442
- ): ReadonlyArray<ReviewFinding> => {
443
- const byAnchor = new Map<string, ReviewFinding>();
444
- for (const finding of findings) {
445
- const key = anchorKey(finding);
446
- const existing = byAnchor.get(key);
447
- if (
448
- existing === undefined ||
449
- severityRank[finding.severity] < severityRank[existing.severity]
450
- ) {
451
- byAnchor.set(key, finding);
452
- }
453
- }
454
- return [...byAnchor.values()]
455
- .sort((left, right) => {
456
- const bySeverity = severityRank[left.severity] - severityRank[right.severity];
457
- if (bySeverity !== 0) return bySeverity;
458
- if (left.path !== right.path) return left.path < right.path ? -1 : 1;
459
- return left.startLine - right.startLine;
460
- })
461
- .slice(0, MAX_MERGED_FINDINGS);
462
- };
463
-
464
- /**
465
- * Stable identity for one concern. The paths are part of the claim: identical
466
- * prose about two independent files must not collapse into one item.
467
- */
468
- export const reviewConcernKey = (concern: ReviewConcern): string =>
469
- `${(concern.evidencePaths ?? []).join("\u0000")}\u0001${concern.title}\u0000${concern.body}`;
470
-
471
- /**
472
- * The concern analogue of `rankAndDedupeFindings`: dedupe by exact scoped
473
- * content keeping the most severe duplicate, rank by severity, and cap at the
474
- * `CodeReview` concerns bound.
475
- */
476
- export const rankAndDedupeConcerns = (
477
- concerns: ReadonlyArray<ReviewConcern>,
478
- ): ReadonlyArray<ReviewConcern> => {
479
- const byContent = new Map<string, ReviewConcern>();
480
- for (const concern of concerns) {
481
- const key = reviewConcernKey(concern);
482
- const previous = byContent.get(key);
483
- if (
484
- previous === undefined ||
485
- severityRank[concern.severity] < severityRank[previous.severity]
486
- ) {
487
- byContent.set(key, concern);
488
- }
489
- }
490
- return [...byContent.values()]
491
- .sort((left, right) => severityRank[left.severity] - severityRank[right.severity])
492
- .slice(0, 10);
493
- };