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

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 +11 -204
  2. package/dist/index.d.mts +92 -914
  3. package/dist/index.mjs +176 -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 +244 -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,357 +0,0 @@
1
- import { Effect, Layer, Schema } from "effect";
2
- import {
3
- Agent,
4
- AgentPolicy,
5
- getToolExecutionClass,
6
- IdGenerator,
7
- type AgentPolicyInput,
8
- type UsageBudgetLimits,
9
- } from "effect-agent";
10
- import { Toolkit, type LanguageModel, type Model, type Tool } from "effect/unstable/ai";
11
-
12
- import type { ChangedFile } from "./diff.ts";
13
- import { makeFileReviewerDefinition } from "./fan-out.ts";
14
- import { computeChangesetFingerprint, computeProfileFingerprint } from "./fingerprint.ts";
15
- import { compileIgnoreGlobs, ignoringPullRequestSourceLayer } from "./ignore.ts";
16
- import {
17
- clampMaxFindings,
18
- CodeReview,
19
- defaultReviewPolicy,
20
- ListChangedFiles,
21
- makeReviewInstructions,
22
- ReadFile,
23
- ReadFileDiff,
24
- ReviewMission,
25
- ReviewToolkitLayer,
26
- resolveGuidance as resolveReviewGuidance,
27
- type ReviewGuidance,
28
- } from "./review-agent.ts";
29
- import { buildProfileMission } from "./review-state.ts";
30
- import {
31
- buildReviewMission,
32
- executeFanOutReview,
33
- executeReview,
34
- fanOutReviewBudgetLimits,
35
- reviewBudgetLimits,
36
- } from "./run.ts";
37
- import { PullRequestSource } from "./source.ts";
38
-
39
- // ---------------------------------------------------------------------------
40
- // The configuration factory: one call turns a Model and optional adaptation
41
- // knobs into a bound, runnable reviewer. Every knob widens what goes INTO the
42
- // review — guidance, extra read-only tools, execution bounds, ignore globs —
43
- // and none weakens what leaves it: anchor validation, the findings bound, and
44
- // publication-after-settlement are applied by the run path unconditionally.
45
- // ---------------------------------------------------------------------------
46
-
47
- /** Options shared by both reviewer shapes. */
48
- export interface PrReviewSharedOptions {
49
- /**
50
- * Host-side and instruction-level findings bound, clamped to the CodeReview
51
- * schema cap of 20.
52
- */
53
- readonly maxFindings?: number | undefined;
54
- /**
55
- * Glob patterns (`**` crosses directories, `*`/`?` stay in one segment)
56
- * removed from the reviewer's observation surface entirely.
57
- */
58
- readonly ignore?: ReadonlyArray<string> | undefined;
59
- /** Map the model's verdict onto APPROVE/REQUEST_CHANGES instead of COMMENT. */
60
- readonly applyVerdict?: boolean | undefined;
61
- /** Run-level usage bounds; defaults to the shape's packaged limits. */
62
- readonly budget?: UsageBudgetLimits | undefined;
63
- /**
64
- * Human-readable descriptor of the bound model (provider, model id, effort,
65
- * and request profile such as service tier)
66
- * rendered into the review footer and included in the fingerprint
67
- * signature, so changing the binding re-reviews instead of skipping.
68
- */
69
- readonly modelLabel?: string | undefined;
70
- }
71
-
72
- /** Options accepted by `PrReview.make` (the flat reviewer). */
73
- export interface PrReviewOptions<
74
- Provider,
75
- ModelProvides,
76
- ModelRequires,
77
- Extra extends ReadonlyArray<Tool.Any>,
78
- > extends PrReviewSharedOptions {
79
- /** The Effect AI Model to bind; its Layer requirements stay visible in `R`. */
80
- readonly model: Model.Model<Provider, LanguageModel.LanguageModel | ModelProvides, ModelRequires>;
81
- /** Domain guidance injected between the mission framing and the procedure. */
82
- readonly guidance?: ReviewGuidance | undefined;
83
- /** Full override of the flat reviewer's execution bounds. */
84
- readonly policy?: AgentPolicyInput | undefined;
85
- /**
86
- * Additional tools merged into the reviewer's toolkit. Every extra tool
87
- * must be annotated `ToolExecutionClass: "readonly"` — construction fails
88
- * otherwise — and its handler Layer is the caller's to provide, so the new
89
- * dependency stays visible in the run's `R`.
90
- */
91
- readonly extraTools?: Extra | undefined;
92
- }
93
-
94
- /** How one run should publish. */
95
- export interface RunReviewOptions {
96
- /** Post the review to GitHub; `false` (default) stops after planning. */
97
- readonly post?: boolean | undefined;
98
- /** Workflow-run URL rendered into the review footer. */
99
- readonly runUrl?: string | undefined;
100
- }
101
-
102
- const EMPTY_TOOLS: ReadonlyArray<Tool.Any> = [];
103
-
104
- const requireReadonly = (tools: ReadonlyArray<Tool.Any>): void => {
105
- for (const tool of tools) {
106
- const executionClass = getToolExecutionClass(tool);
107
- if (executionClass !== "readonly") {
108
- throw new Error(
109
- `PrReview.make: extra tool '${tool.name}' declares execution class '${executionClass}'. ` +
110
- `The packaged reviewer's tool surface is read-only; annotate the tool with ` +
111
- `ToolExecutionClass "readonly" or run it outside the reviewer.`,
112
- );
113
- }
114
- }
115
- };
116
-
117
- const provideIgnore = <A, E, R>(
118
- effect: Effect.Effect<A, E, R>,
119
- ignore: ReadonlyArray<string> | undefined,
120
- ) =>
121
- ignore !== undefined && ignore.length > 0
122
- ? effect.pipe(Effect.provide(ignoringPullRequestSourceLayer(ignore)))
123
- : effect;
124
-
125
- /**
126
- * The changeset fingerprint of what this reviewer WOULD review right now:
127
- * the ignore-filtered changeset hashed with the prompt signature. Identical
128
- * fingerprints mean an identical review input surface — the basis for
129
- * skipping re-reviews after content-free head changes (base auto-merges,
130
- * equivalent rebases).
131
- */
132
- const makeFingerprint = (
133
- signature: (mission: ReturnType<typeof buildReviewMission>) => string,
134
- ignore: ReadonlyArray<string> | undefined,
135
- ) =>
136
- provideIgnore(
137
- Effect.gen(function* () {
138
- const source = yield* PullRequestSource;
139
- const metadata = yield* source.metadata;
140
- const files = yield* source.changedFiles;
141
- return yield* computeChangesetFingerprint(
142
- files,
143
- signature(buildReviewMission(metadata, files)),
144
- );
145
- }),
146
- ignore,
147
- );
148
-
149
- const makeProfileFingerprint = (
150
- signature: (mission: ReviewMission) => string,
151
- ignore: ReadonlyArray<string> | undefined,
152
- ) =>
153
- provideIgnore(
154
- Effect.gen(function* () {
155
- const source = yield* PullRequestSource;
156
- const metadata = yield* source.metadata;
157
- const files = yield* source.anchorFiles;
158
- return yield* computeProfileFingerprint(signature(buildProfileMission(metadata, files)));
159
- }),
160
- ignore,
161
- );
162
-
163
- const makeReviewSnapshot = (ignore: ReadonlyArray<string> | undefined) =>
164
- provideIgnore(
165
- Effect.gen(function* () {
166
- const source = yield* PullRequestSource;
167
- return {
168
- metadata: yield* source.metadata,
169
- files: yield* source.anchorFiles,
170
- };
171
- }),
172
- ignore,
173
- );
174
-
175
- /**
176
- * Build the flat reviewer: one bounded read-only agent over the whole
177
- * changeset. Returns the model-agnostic definition, the explicit binding, and
178
- * a `run` whose error and requirement channels stay fully inferred — the
179
- * pull-request source, the publisher, `Crypto.Crypto`, extra tool handlers,
180
- * and the Model Layer's requirements all remain visible to the caller.
181
- */
182
- const make = <
183
- Provider,
184
- ModelProvides,
185
- ModelRequires,
186
- const Extra extends ReadonlyArray<Tool.Any> = readonly [],
187
- >(
188
- options: PrReviewOptions<Provider, ModelProvides, ModelRequires, Extra>,
189
- ) => {
190
- // Safe when `extraTools` is omitted: the generic default fixes Extra to the
191
- // empty tuple, which is exactly what the fallback value is.
192
- const extraTools = options.extraTools ?? (EMPTY_TOOLS as Extra);
193
- requireReadonly(extraTools);
194
-
195
- const definition = Agent.define("pr-reviewer", {
196
- input: ReviewMission,
197
- output: CodeReview,
198
- instructions: makeReviewInstructions({
199
- guidance: options.guidance,
200
- maxFindings: options.maxFindings,
201
- }),
202
- toolkit: Toolkit.make(ListChangedFiles, ReadFileDiff, ReadFile, ...extraTools),
203
- policy: options.policy === undefined ? defaultReviewPolicy : AgentPolicy.make(options.policy),
204
- description:
205
- "Review one pull request read-only: list the changeset, read annotated diffs and head-file context, and return a structured, line-anchored code review.",
206
- metadata: { deploymentClass: "E", surface: "read-only" },
207
- });
208
- // `Agent.withModel` types the model through a conditional that stays
209
- // deferred inside this generic body, so the binding is built structurally —
210
- // the identical frozen `{ definition, model }` pair the runtime accepts.
211
- const binding = Object.freeze({ definition, model: options.model });
212
-
213
- // Everything that shapes this reviewer's output: the rendered instructions
214
- // (mission, guidance, findings bound, contract) plus the verdict mapping.
215
- const signature = (mission: ReviewMission): string =>
216
- [
217
- definition.instructions(mission),
218
- `applyVerdict=${String(options.applyVerdict ?? false)}`,
219
- ...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
220
- ].join("\u0000");
221
- const profileSignature = (mission: ReviewMission): string =>
222
- [
223
- "pr-review-profile-v1-flat",
224
- JSON.stringify(resolveReviewGuidance(options.guidance, mission)),
225
- JSON.stringify(options.policy ?? {}),
226
- JSON.stringify(extraTools.map((tool) => tool.name)),
227
- JSON.stringify(options.ignore ?? []),
228
- `maxFindings=${clampMaxFindings(options.maxFindings)}`,
229
- `applyVerdict=${String(options.applyVerdict ?? false)}`,
230
- ...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
231
- ].join("\u0000");
232
-
233
- const run = (runOptions: RunReviewOptions = {}) =>
234
- provideIgnore(
235
- executeReview(binding, {
236
- post: runOptions.post ?? false,
237
- applyVerdict: options.applyVerdict ?? false,
238
- limits: options.budget ?? reviewBudgetLimits,
239
- maxFindings: clampMaxFindings(options.maxFindings),
240
- signature,
241
- modelLabel: options.modelLabel,
242
- runUrl: runOptions.runUrl,
243
- }).pipe(Effect.provide(Layer.mergeAll(ReviewToolkitLayer, IdGenerator.layer)), Effect.scoped),
244
- options.ignore,
245
- );
246
-
247
- return {
248
- definition,
249
- binding,
250
- run,
251
- fingerprint: makeFingerprint(signature, options.ignore),
252
- profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
253
- snapshot: makeReviewSnapshot(options.ignore),
254
- filterFiles: (files: ReadonlyArray<ChangedFile>) => {
255
- const ignored = compileIgnoreGlobs(options.ignore ?? []);
256
- return files.filter((file) => !ignored(file.path));
257
- },
258
- } as const;
259
- };
260
-
261
- /** Options accepted by `PrReview.makeFanOut` (the host-scheduled pipeline). */
262
- export interface PrReviewFanOutOptions<
263
- Provider,
264
- ModelProvides,
265
- ModelRequires,
266
- > extends PrReviewSharedOptions {
267
- /** The Effect AI Model bound to every child review pass. */
268
- readonly model: Model.Model<Provider, LanguageModel.LanguageModel | ModelProvides, ModelRequires>;
269
- /**
270
- * Static guidance injected into every child reviewer's instructions. The
271
- * pipeline schedules children from the deterministic plan, so
272
- * mission-dependent guidance cannot exist for children.
273
- */
274
- readonly guidance?: string | ReadonlyArray<string> | undefined;
275
- }
276
-
277
- /**
278
- * Build the fan-out reviewer: host code schedules host-planned general and
279
- * specialist discovery plus independent candidate verification directly as
280
- * bounded child runs — there is no coordinator model and no delegation tool,
281
- * so review assurance cannot fail on scheduling compliance. Host code
282
- * composes publication only from exactly confirmed candidates. Child
283
- * execution bounds are packaged and not configurable here.
284
- */
285
- const makeFanOut = <Provider, ModelProvides, ModelRequires>(
286
- options: PrReviewFanOutOptions<Provider, ModelProvides, ModelRequires>,
287
- ) => {
288
- const child = makeFileReviewerDefinition({ guidance: options.guidance });
289
- // Structural binding for the same reason as in `make` above.
290
- const childBinding = Object.freeze({ definition: child, model: options.model });
291
-
292
- // Everything that shapes this pipeline's output: the child instructions
293
- // (guidance included) plus the host knobs the children do not carry.
294
- const guidanceLines =
295
- options.guidance === undefined
296
- ? []
297
- : typeof options.guidance === "string"
298
- ? [options.guidance]
299
- : options.guidance;
300
- const signature = (mission: ReviewMission): string =>
301
- [
302
- "pr-review-fan-out-host-scheduled-v1",
303
- // Children never see the mission, but the documented skip-unchanged
304
- // contract includes pull-request framing, so the fingerprint keeps it.
305
- JSON.stringify(Schema.encodeSync(ReviewMission)(mission)),
306
- `childGuidance=${JSON.stringify(guidanceLines)}`,
307
- `maxFindings=${clampMaxFindings(options.maxFindings)}`,
308
- `applyVerdict=${String(options.applyVerdict ?? false)}`,
309
- ...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
310
- ].join(" ");
311
- const profileSignature = (_mission: ReviewMission): string =>
312
- [
313
- // v4 invalidates continuity produced by the coordinator-scheduled
314
- // pipeline; the host now schedules every pass and retries failures.
315
- "pr-review-profile-v4-host-scheduled",
316
- JSON.stringify(guidanceLines),
317
- JSON.stringify(options.ignore ?? []),
318
- `maxFindings=${clampMaxFindings(options.maxFindings)}`,
319
- `applyVerdict=${String(options.applyVerdict ?? false)}`,
320
- ...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
321
- ].join(" ");
322
-
323
- const run = (runOptions: RunReviewOptions = {}) =>
324
- provideIgnore(
325
- executeFanOutReview(childBinding, {
326
- post: runOptions.post ?? false,
327
- applyVerdict: options.applyVerdict ?? false,
328
- limits: options.budget ?? fanOutReviewBudgetLimits,
329
- maxFindings: clampMaxFindings(options.maxFindings),
330
- signature,
331
- modelLabel: options.modelLabel,
332
- runUrl: runOptions.runUrl,
333
- }).pipe(Effect.provide(IdGenerator.layer), Effect.scoped),
334
- options.ignore,
335
- );
336
-
337
- return {
338
- definition: child,
339
- childBinding,
340
- run,
341
- fingerprint: makeFingerprint(signature, options.ignore),
342
- profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
343
- snapshot: makeReviewSnapshot(options.ignore),
344
- filterFiles: (files: ReadonlyArray<ChangedFile>) => {
345
- const ignored = compileIgnoreGlobs(options.ignore ?? []);
346
- return files.filter((file) => !ignored(file.path));
347
- },
348
- } as const;
349
- };
350
-
351
- /**
352
- * The packaged pull-request reviewer factory.
353
- *
354
- * - `make` — one flat reviewer over the whole changeset.
355
- * - `makeFanOut` — a coordinator delegating bounded per-unit child reviews.
356
- */
357
- export const PrReview = { make, makeFanOut } as const;
@@ -1,77 +0,0 @@
1
- import { Effect, Layer, Ref, Schema, Stream } from "effect";
2
- import { LanguageModel, Model } from "effect/unstable/ai";
3
-
4
- import { FileReviewReport } from "./fan-out.ts";
5
- import { scriptedFinalParts } from "./scripted.ts";
6
-
7
- // Deterministic offline child models for the host-scheduled fan-out pipeline.
8
- // Decisions key on the work ID committed into each child's instructions,
9
- // never call order, so bounded pass concurrency cannot make fixtures flaky.
10
- // Each work ID carries a SEQUENCE of outcomes consumed per call, so tests can
11
- // script "fail once, then settle" and pin the pipeline's bounded retry.
12
-
13
- export type OfflineUnitOutcome =
14
- | { readonly _tag: "report"; readonly report: FileReviewReport }
15
- | { readonly _tag: "malformed-output" };
16
-
17
- export interface OfflineUnitScript {
18
- readonly workId: string;
19
- /** Consumed one per child call for this work ID; the last outcome repeats. */
20
- readonly outcomes: ReadonlyArray<OfflineUnitOutcome>;
21
- }
22
-
23
- export const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitScript>) =>
24
- Effect.gen(function* () {
25
- const calls = yield* Ref.make(0);
26
- const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
27
- const callsByWorkId = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
28
- const model = Model.make(
29
- "scripted",
30
- "pr-fanout-file-reviewer-offline",
31
- Layer.effect(
32
- LanguageModel.LanguageModel,
33
- LanguageModel.make({
34
- generateText: () => Effect.succeed([]),
35
- streamText: (request) =>
36
- Stream.unwrap(
37
- Effect.gen(function* () {
38
- yield* Ref.update(calls, (value) => value + 1);
39
- const promptJson = JSON.stringify(request.prompt);
40
- yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
41
- const script = scripts.filter((candidate) =>
42
- promptJson.includes(`for ${candidate.workId} in host-planned unit`),
43
- );
44
- if (script.length !== 1) {
45
- return yield* Effect.die(
46
- new Error("The child prompt must name exactly one scripted work ID"),
47
- );
48
- }
49
- const [selected] = script;
50
- if (selected === undefined) return yield* Effect.die("unreachable scripted match");
51
- const occurrence = yield* Ref.modify(callsByWorkId, (byWorkId) => {
52
- const count = byWorkId.get(selected.workId) ?? 0;
53
- const next = new Map(byWorkId);
54
- next.set(selected.workId, count + 1);
55
- return [count, next] as const;
56
- });
57
- const outcome =
58
- selected.outcomes[Math.min(occurrence, selected.outcomes.length - 1)];
59
- if (outcome === undefined) {
60
- return yield* Effect.die(
61
- new Error(`Work ID ${selected.workId} scripts no outcomes`),
62
- );
63
- }
64
- return Stream.fromIterable(
65
- scriptedFinalParts(
66
- outcome._tag === "report"
67
- ? JSON.stringify(Schema.encodeSync(FileReviewReport)(outcome.report))
68
- : "this is not valid review JSON",
69
- ),
70
- );
71
- }),
72
- ),
73
- }),
74
- ),
75
- );
76
- return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };
77
- });