@effect-agent/pr-review 0.1.0-beta.24 → 0.1.0-beta.26
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 +83 -202
- package/dist/action.d.mts +25 -16
- package/dist/action.mjs +51 -39
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/{fan-out-Bi1v0VaU.d.mts → fan-out-CMEsbFLk.d.mts} +432 -206
- package/dist/{github-C6jrBLA2.mjs → github-NjgxGqwM.mjs} +2141 -1658
- package/dist/github-NjgxGqwM.mjs.map +1 -0
- package/dist/index.d.mts +20 -12
- package/dist/index.mjs +3 -3
- package/dist/{providers-2Jao2ZAX.mjs → providers-CODZQCmL.mjs} +192 -79
- package/dist/providers-CODZQCmL.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/src/action.ts +123 -78
- package/src/cli.ts +6 -1
- package/src/index.ts +1 -0
- package/src/internal/adjudication.ts +415 -0
- package/src/internal/coverage.ts +41 -60
- package/src/internal/fan-out.ts +56 -4
- package/src/internal/github-env.ts +9 -0
- package/src/internal/github.ts +207 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +186 -42
- package/src/internal/retirement.ts +16 -17
- package/src/internal/review-agent.ts +39 -4
- package/src/internal/review-state.ts +161 -42
- package/src/internal/review-units.ts +10 -9
- package/src/internal/run.ts +178 -64
- package/dist/github-C6jrBLA2.mjs.map +0 -1
- package/dist/providers-2Jao2ZAX.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @effect-agent/pr-review
|
|
2
2
|
|
|
3
|
-
A bounded, fail-closed GitHub pull-request reviewer built on
|
|
4
|
-
|
|
3
|
+
A bounded, fail-closed GitHub pull-request reviewer built on effect-agent's public APIs. Read-only
|
|
4
|
+
Agents review host-partitioned pull-request evidence
|
|
5
5
|
through typed ports; the host validates every finding anchor against the real
|
|
6
6
|
diff and posts one review after the run settles.
|
|
7
7
|
|
|
@@ -15,6 +15,7 @@ truncated run posts nothing.
|
|
|
15
15
|
import { Effect, Layer } from "effect";
|
|
16
16
|
import {
|
|
17
17
|
PrReview,
|
|
18
|
+
fullReviewExecutionContextLayer,
|
|
18
19
|
gitHubReviewLayers,
|
|
19
20
|
resolveReviewTarget,
|
|
20
21
|
makeOpenAiReviewModel,
|
|
@@ -27,13 +28,22 @@ const program = Effect.gen(function* () {
|
|
|
27
28
|
const target = yield* resolveReviewTarget({ repository: "acme/api", number: 123 });
|
|
28
29
|
return yield* reviewer
|
|
29
30
|
.run({ post: true })
|
|
30
|
-
.pipe(
|
|
31
|
+
.pipe(
|
|
32
|
+
Effect.provide(
|
|
33
|
+
fullReviewExecutionContextLayer("explicit direct full review").pipe(
|
|
34
|
+
Layer.provideMerge(Layer.merge(gitHubReviewLayers(target), openAiClientLayer)),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
31
38
|
});
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
The run's requirement channel keeps every real dependency visible: the
|
|
35
|
-
`PullRequestSource
|
|
36
|
-
|
|
42
|
+
`PullRequestSource`, `ReviewPublisher`, `ReviewAdjudicationHost`, and
|
|
43
|
+
`ReviewExecutionContext` ports, the provider client, and the handler Layer of
|
|
44
|
+
any extra tool you add. Direct callers provide an explicit full-review context
|
|
45
|
+
as above; the packaged action supplies its selected incremental or full range.
|
|
46
|
+
Anthropic is equally supported
|
|
37
47
|
(`makeAnthropicReviewModel`, `anthropicClientLayer`), and the factory accepts
|
|
38
48
|
any Effect AI Model.
|
|
39
49
|
|
|
@@ -53,44 +63,20 @@ const reviewer = PrReview.make({
|
|
|
53
63
|
});
|
|
54
64
|
```
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
the
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pass that fails (child fault, malformed or misdirected output) is retried
|
|
71
|
-
once; a pass that stays failed is reported and its unit's paths carry forward
|
|
72
|
-
as retryable scope. Verifiers receive the exact candidate claims and the
|
|
73
|
-
complete bounded unit, including neighboring evidence that can falsify a
|
|
74
|
-
locally plausible claim; they do not receive discovery reasoning.
|
|
75
|
-
The host builds publishable findings and concerns from exact confirmed
|
|
76
|
-
candidate IDs and deduplicates byte-identical cross-pass claims in
|
|
77
|
-
deterministic plan order; a discovery claim anchored outside its assigned
|
|
78
|
-
evidence is discarded and counted, never published and never pass-fatal. The
|
|
79
|
-
summary and verdict are composed deterministically from the confirmed
|
|
80
|
-
severities. Shared `guidance` reaches both discovery and verification;
|
|
81
|
-
`maxFindings` remains a host-enforced publication cap.
|
|
82
|
-
|
|
83
|
-
GitHub may omit the `patch` field for large textual files as well as binary
|
|
84
|
-
files. The GitHub source recovers a missing patch by reading bounded, strict
|
|
85
|
-
UTF-8 base/head content: additions require the head, deletions require the
|
|
86
|
-
base, and other changes require both. Reviewers receive that content through
|
|
87
|
-
the ordinary diff-read tool with non-anchorable `B`/`H` line labels and must
|
|
88
|
-
report defects as review-body concerns. Invalid UTF-8, binary NUL content,
|
|
89
|
-
missing sides, and files beyond the per-side read bound leave a path with no
|
|
90
|
-
reviewable textual evidence. Such paths keep input coverage incomplete and are
|
|
91
|
-
carried for as long as they are part of the pull request — an unreviewable
|
|
92
|
-
change must never authorize a green check; exclude them deliberately with
|
|
93
|
-
ignore globs when that is intended.
|
|
66
|
+
Model output is untrusted. The host revalidates anchors, enforces `maxFindings`, and publishes only
|
|
67
|
+
after the run settles. `PrReview.makeFanOut` partitions bounded evidence and schedules general and
|
|
68
|
+
specialist discovery for every unit, followed by independent candidate verification. Verifiers
|
|
69
|
+
receive the exact claims and enough neighboring evidence to reject a plausible mistake, but no
|
|
70
|
+
discovery reasoning.
|
|
71
|
+
|
|
72
|
+
Each failed pass retries once, then carries its unit forward. The host accepts only confirmed
|
|
73
|
+
candidate IDs, discards anchors outside assigned evidence, deduplicates claims in plan order, and
|
|
74
|
+
derives the verdict from validated severities. Shared `guidance` reaches discovery and verification.
|
|
75
|
+
|
|
76
|
+
When GitHub omits a textual patch, the source reads bounded, strict UTF-8 base/head content and
|
|
77
|
+
labels it with non-anchorable `B`/`H` lines. Invalid UTF-8, binary NUL content, missing sides, and
|
|
78
|
+
oversized files remain unreviewable and keep input coverage incomplete. Exclude them with ignore
|
|
79
|
+
globs only when that is intentional.
|
|
94
80
|
|
|
95
81
|
## Assurance model
|
|
96
82
|
|
|
@@ -117,51 +103,52 @@ favors redundant work, but it cannot recognize every semantically risky
|
|
|
117
103
|
change. The specialist pass is context-independent redundancy, not a claim of
|
|
118
104
|
provider or model diversity. Running it for every unit prevents classifier
|
|
119
105
|
silence from suppressing scrutiny; the category labels still cannot prove
|
|
120
|
-
that every semantic risk was recognized.
|
|
121
|
-
for compatibility; new hosts and UI use `inputCoverage` and `assurance`.
|
|
106
|
+
that every semantic risk was recognized. Hosts and UI use `inputCoverage` and `assurance`.
|
|
122
107
|
The flat reviewer has path-input accounting but no independent verifier, so
|
|
123
108
|
its assurance is `unverified` and the Action check cannot report success from
|
|
124
109
|
that shape.
|
|
125
110
|
|
|
111
|
+
## Maintainer adjudication
|
|
112
|
+
|
|
113
|
+
A maintainer can settle a finding without changing code, from the pull request itself:
|
|
114
|
+
|
|
115
|
+
- On a finding's inline thread, reply `/adjudicate accepted-risk|refuted|obsolete[: reason]`.
|
|
116
|
+
The thread names the target, so the verb alone suffices.
|
|
117
|
+
- For an unanchored concern, comment
|
|
118
|
+
`/adjudicate <disposition> "<exact concern title>"[: reason]` in the PR conversation. The quoted
|
|
119
|
+
title is required and must match exactly.
|
|
120
|
+
|
|
121
|
+
An adjudicated identity leaves active findings, verdict counts, and the check conclusion; it
|
|
122
|
+
renders in a collapsed "Adjudicated" section instead, and the final audit distinguishes fixed,
|
|
123
|
+
adjudicated, and still-open items. The reviewer prompt names each adjudication so the model does
|
|
124
|
+
not re-raise it without materially new evidence. Identity is exact — path, line range, and title
|
|
125
|
+
for findings, title alone for concerns — so a materially different finding at the same location is
|
|
126
|
+
untouched. Adjudications persist in the signed review state, and the skip-unchanged path re-reads
|
|
127
|
+
them, so an adjudication lifts a blocking check without a new commit.
|
|
128
|
+
|
|
129
|
+
**Authorization is fail-closed**: only comments whose `author_association` is OWNER, MEMBER, or
|
|
130
|
+
COLLABORATOR adjudicate. Anything else — third parties, bots, malformed commands — is ignored
|
|
131
|
+
(logged at debug). Later adjudications of the same identity win by comment creation order, bounded
|
|
132
|
+
at 20 stored entries (oldest dropped with a logged notice). If either GitHub listing surface fails,
|
|
133
|
+
or one thread exceeds the bounded authorized-command history, fresh collection is discarded and
|
|
134
|
+
the stored set stands unchanged.
|
|
135
|
+
|
|
136
|
+
**Rejected alternative — parsing free-text rebuttals** ("this is fine because …" replies): only an
|
|
137
|
+
explicit, authorized verb is auditable and fail-closed. Inferring intent from prose would let model
|
|
138
|
+
output or third-party comments silently suppress findings, and nobody could later say which comment
|
|
139
|
+
dismissed what.
|
|
140
|
+
|
|
126
141
|
## What a posted review looks like
|
|
127
142
|
|
|
128
|
-
The
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
never from model prose. Directly under it, one host-derived stats line names
|
|
132
|
-
the changeset size (`N files (+adds / −dels)`), the severity tally, and a
|
|
133
|
-
deterministic 1–5 review-effort estimate computed from the changeset shape
|
|
134
|
-
alone. Fan-out reviews add a second host-derived line naming input assignment,
|
|
135
|
-
discovery/specialist/verification settlement, and candidate dispositions.
|
|
136
|
-
Below the summary, a collapsed **📝 Walkthrough** table carries the
|
|
137
|
-
model's one-sentence per-file change summaries — walkthrough paths are
|
|
138
|
-
validated like finding anchors, so entries naming files outside the changeset
|
|
139
|
-
are dropped. Under fan-out only summaries a
|
|
140
|
-
successfully settled general pass actually reported for its own unit's paths
|
|
141
|
-
survive, so a child cannot smuggle or invent entries. Non-anchored `concerns`
|
|
142
|
-
(deletion plans, rollout sequencing,
|
|
143
|
-
coverage gaps, scope questions — things with no diff line to point at) render
|
|
144
|
-
as severity-tagged sections; demoted findings and findings carried from
|
|
145
|
-
unchanged scope collapse into counted `<details>` sections.
|
|
146
|
-
|
|
147
|
-
Each inline comment is headed by its severity plus an optional model-claimed
|
|
148
|
-
category chip (`**[⚠️ important · security] …**`), carries a committable
|
|
149
|
-
GitHub `suggestion` fence when independent verification settled the model's
|
|
150
|
-
suggestion as committable replacement source, and ends with a
|
|
151
|
-
collapsed **🤖 Prompt for AI agents** — a copy-paste instruction derived
|
|
152
|
-
host-side from the validated finding, opening with a fixed preamble telling
|
|
153
|
-
the receiving agent to treat the finding content as untrusted review data.
|
|
154
|
-
The review body repeats every finding — anchored, demoted, and carried — in
|
|
155
|
-
one consolidated prompt block so nothing needs to be collected by hand. The
|
|
156
|
-
footer names the model binding, the observed token usage, and links to the
|
|
157
|
-
workflow run; an invisible metadata comment pins the reviewed head commit so
|
|
158
|
-
later readers know when line callouts have gone stale.
|
|
143
|
+
The host derives callouts, statistics, walkthroughs, inline comments, and the final prompt from
|
|
144
|
+
validated findings. Model prose cannot choose the verdict or smuggle paths outside the changeset.
|
|
145
|
+
The [Action guide](../../action/README.md) documents the rendered review and workflow outputs.
|
|
159
146
|
|
|
160
147
|
## Swap a port
|
|
161
148
|
|
|
162
149
|
Tools observe the pull request only through `PullRequestSource`; publication
|
|
163
150
|
happens only through `ReviewPublisher`. Provide your own Layers to review
|
|
164
|
-
|
|
151
|
+
any diff-shaped input or publish elsewhere. The GitHub REST adapters are
|
|
165
152
|
one implementation, not the contract.
|
|
166
153
|
|
|
167
154
|
## Test what you adapted
|
|
@@ -176,131 +163,25 @@ import {
|
|
|
176
163
|
```
|
|
177
164
|
|
|
178
165
|
Deterministic in-memory adapters for both ports plus prompt-keyed scripted
|
|
179
|
-
models that
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
##
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
and
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
validates the state and reviews the GitHub comparison from that reviewed head
|
|
192
|
-
to the current head plus the carried unreviewed paths, not the complete
|
|
193
|
-
base...HEAD diff. When that head is not a git ancestor — a rebase, amend, or
|
|
194
|
-
force-push — the host falls back to a two-dot tree comparison and reviews only
|
|
195
|
-
the current PR paths whose contents actually changed, plus leftovers. Unchanged
|
|
196
|
-
settled scope is not sent back to the model; unchanged unresolved findings
|
|
197
|
-
remain active; changed or reverted paths invalidate their prior findings and
|
|
198
|
-
receive fresh discovery and verification. An unchanged leftover from a failed
|
|
199
|
-
pass retries only that failed stage and keeps its stored findings — it does
|
|
200
|
-
not run a second general discovery.
|
|
201
|
-
Whole overflow files are reviewed in bounded installments across pushes
|
|
202
|
-
through the same carry; a single file beyond total plan capacity and
|
|
203
|
-
undiffable paths stay explicitly incomplete instead — an unreviewable tail
|
|
204
|
-
never moves behind a green check. Non-anchored concerns are carried conservatively
|
|
205
|
-
until a full audit because they cannot be mapped safely to one path.
|
|
206
|
-
|
|
207
|
-
The state marker must be terminal, signed with the configured stable
|
|
208
|
-
`PR_REVIEW_STATE_SECRET`, authored by the configured review-posting bot, and
|
|
209
|
-
pinned to the reviewed commit. The expected author defaults to
|
|
210
|
-
`github-actions[bot]`; set `PR_REVIEW_AUTHOR_LOGIN` (or the Action's
|
|
211
|
-
`review-author` input) to `<app-slug>[bot]` when posting with a custom GitHub
|
|
212
|
-
App token. State lookup, authentication, schema, identity, ancestry, profile,
|
|
213
|
-
and comparison checks are fail-closed for scope selection: missing, stale,
|
|
214
|
-
incompatible, or truncated state/comparisons produce a visible full-diff
|
|
215
|
-
fallback unless a two-dot content comparison can still name the changed PR
|
|
216
|
-
paths. An ancestor base advance remains incremental and adds overlapping
|
|
217
|
-
PR paths as affected context; a materially changed base lineage falls back to
|
|
218
|
-
full unless the authenticated settled-scope fingerprint still matches or the
|
|
219
|
-
content comparison intersects the current PR path set. The
|
|
220
|
-
schema field retains the compatibility name `acceptedScopeFingerprint`. That
|
|
221
|
-
fingerprint hashes the ignore-filtered effective diff, patchless base/head
|
|
222
|
-
evidence, PR framing, and review-shaping profile while excluding commit IDs,
|
|
223
|
-
base ancestry, and unified-diff hunk coordinates. Re-running the same head or a
|
|
224
|
-
patch-equivalent rebase skips model execution by default ONLY when the stored
|
|
225
|
-
state fully settled — a state carrying unreviewed scope is always retried.
|
|
226
|
-
The preserved skip keeps its stored blocking/success conclusion and posts no
|
|
227
|
-
duplicate review comments. Missing/corrupt state and any fingerprint or
|
|
228
|
-
profile mismatch review conservatively.
|
|
229
|
-
|
|
230
|
-
After a new state-bearing Action review posts, prior marker-bearing bot reviews
|
|
231
|
-
are retired by default: their bodies become collapsed, superseded history,
|
|
232
|
-
resolved findings are struck through, and matching inline comments are
|
|
233
|
-
minimized as outdated. Only strictly older reviews from the same GitHub actor
|
|
234
|
-
are eligible, so copied markers and newer concurrent reviews are untouched.
|
|
235
|
-
The machine-state comments remain byte-identical and terminal, so an edited
|
|
236
|
-
body still participates in incremental state recovery.
|
|
237
|
-
This cosmetic pass is fail-open and can be disabled with the Action input
|
|
238
|
-
`retire-stale-reviews: "false"`.
|
|
239
|
-
|
|
240
|
-
Authentication is an explicit Effect service supplied by the Action host;
|
|
241
|
-
WebCrypto import/sign/verify failures stay typed. The terminal marker is
|
|
242
|
-
schema-branded and capped at 24,000 characters. If signing fails or state
|
|
243
|
-
exceeds that bound, the completed review is posted without continuity state
|
|
244
|
-
and with a bounded warning, so the next run safely performs a full review.
|
|
245
|
-
|
|
246
|
-
`review-mode: final` is the explicit bounded merge-readiness audit. It ignores
|
|
247
|
-
prior assurance state, plans fresh discovery over the full current PR
|
|
248
|
-
diff, verifies new candidates, and resets the incremental baseline; normal
|
|
249
|
-
`synchronize` events use `incremental` and do not perform this audit.
|
|
250
|
-
Because equivalence is based on the textual review surface, a base change that
|
|
251
|
-
alters runtime meaning without changing the diff/context is not detectable;
|
|
252
|
-
request `final` mode (or disable unchanged skipping) for that case.
|
|
253
|
-
|
|
254
|
-
## Hosts
|
|
255
|
-
|
|
256
|
-
- **GitHub Actions**: the repository ships a prebuilt node-runtime action
|
|
257
|
-
supporting a committed review-profile document via `guidance-file` (this
|
|
258
|
-
repository's own profile lives at `.github/review-guidance.md`)
|
|
259
|
-
(`action/` at the repo root) — `uses` it with an API-key secret and nothing
|
|
260
|
-
else. Fan-out is the Action default because the flat compatibility shape
|
|
261
|
-
has no independent verifier and cannot settle review assurance. While a run executes it maintains one sticky, fail-open "review in
|
|
262
|
-
progress" comment updated in place with the settled outcome
|
|
263
|
-
(`progress-comment` input, default on; at-least-once with generation-fenced
|
|
264
|
-
writes and best-effort duplicate cleanup — strict single-comment behavior
|
|
265
|
-
comes from a per-PR workflow concurrency group), and its logs render one
|
|
266
|
-
compact line per event (`log-level` input, default `Info`). For custom reviewers in
|
|
267
|
-
CI, `@effect-agent/pr-review/action` exports `runReviewAction` (event
|
|
268
|
-
resolution, typed draft/non-PR skips, bounded range selection, step
|
|
269
|
-
outputs, and conservative check gate) to harness your own `reviewer.run`;
|
|
270
|
-
pass `progressComment: true` to opt a custom harness into the sticky
|
|
271
|
-
progress comment. A custom fingerprint-only harness remains source-compatible
|
|
272
|
-
but cannot skip model work: unchanged skipping requires the profile fingerprint
|
|
273
|
-
and authenticated state that prove the prior configured work settled.
|
|
274
|
-
- **CLI**: `bun src/cli.ts --repo owner/name --pr 123 [--post] [--provider anthropic] [--fan-out]`
|
|
275
|
-
(also exported as the `./cli` entry).
|
|
276
|
-
|
|
277
|
-
Environment: `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` for the model,
|
|
278
|
-
`PR_REVIEW_STATE_SECRET` to authenticate incremental state,
|
|
279
|
-
`PR_REVIEW_AUTHOR_LOGIN` to match a custom review-posting bot (defaults to
|
|
280
|
-
`github-actions[bot]`),
|
|
281
|
-
`GITHUB_TOKEN` to post (optional for public-repository reads), and the
|
|
282
|
-
standard `GITHUB_REPOSITORY` / `GITHUB_EVENT_PATH` / `GITHUB_API_URL`
|
|
283
|
-
variables inside Actions.
|
|
166
|
+
models that call the real Tools. Tests need no network or credentials and exercise every ordinary
|
|
167
|
+
gate.
|
|
168
|
+
|
|
169
|
+
## Action and CLI
|
|
170
|
+
|
|
171
|
+
The prebuilt GitHub Action handles signed incremental state, fail-closed scope selection, sticky
|
|
172
|
+
progress, stale-review retirement, and final audits. See its [setup and behavior guide](../../action/README.md)
|
|
173
|
+
and [input reference](../../action/action.yml). Custom hosts can import `runReviewAction` from
|
|
174
|
+
`@effect-agent/pr-review/action`.
|
|
175
|
+
|
|
176
|
+
Run the CLI with
|
|
177
|
+
`bun src/cli.ts --repo owner/name --pr 123 [--post] [--provider anthropic] [--fan-out]`.
|
|
284
178
|
|
|
285
179
|
## Bounds, spelled out
|
|
286
180
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
Fan-out capacity overflow is reported, never dropped. Every fan-out unit has
|
|
295
|
-
at most 12 files, 12 complete evidence shards, and 240,000 evidence characters;
|
|
296
|
-
one path may span multiple shards or units when necessary. If the eight-unit
|
|
297
|
-
plan capacity is exhausted, every affected path and the exact shard count are
|
|
298
|
-
reported, with a deterministic identifier sample bounded to one plan's capacity;
|
|
299
|
-
at most eight units produce at most 24 attached children (general and
|
|
300
|
-
specialist discovery plus one verification batch per unit), with child
|
|
301
|
-
concurrency capped at four. Any blocking active finding fails the Action
|
|
302
|
-
check. Any input gap—including an unassigned evidence shard—failed or
|
|
303
|
-
exhausted configured pass, mismatched candidate batch, or unsettled
|
|
304
|
-
verification is non-success rather than green. A settled
|
|
305
|
-
clean result is evidence that these bounded passes completed, not proof that
|
|
306
|
-
no defect exists.
|
|
181
|
+
Every definition has a finite `AgentPolicy` and run-level `UsageBudgetLimits`. The reviewer rejects
|
|
182
|
+
file versions beyond 200k bytes or characters and patchless B/H renderings beyond 220k characters.
|
|
183
|
+
It reads at most 300 changed files. Each fan-out unit holds at most 12 files, 12 complete evidence
|
|
184
|
+
shards, and 240,000 evidence characters. At most eight units produce 24 attached children with
|
|
185
|
+
child concurrency capped at four. The result reports every capacity overflow. Any blocking
|
|
186
|
+
finding, input gap, failed pass, exhausted pass, mismatched candidate batch, or unsettled
|
|
187
|
+
verification prevents a green check.
|
package/dist/action.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Br as ChangedFile, Cn as ReviewStateAuthenticator, Fr as PullRequestMetadata, Ir as PullRequestSource, L as PriorReviews, Lr as PullRequestSourceFailure, P as GitHubApiFailure, Q as ReviewRetirementHost, Qt as ReviewAdjudicationHost, gn as ReviewExecutionContext, vn as ReviewMode, z as ReviewPublisher } from "./fan-out-CMEsbFLk.mjs";
|
|
2
2
|
import { EffortPosition, InvalidEffortInput, ReviewProgressReporter, ReviewProvider, ReviewRunOutcome, ReviewTargetUnresolved, RunReviewOptions } from "./index.mjs";
|
|
3
3
|
import { Config, Effect, FileSystem, Schema } from "effect";
|
|
4
4
|
import { BudgetExceeded } from "effect-agent";
|
|
@@ -92,29 +92,38 @@ type ReviewActionResult = {
|
|
|
92
92
|
* The reviewer surface the action harness drives. `PrReview.make` and
|
|
93
93
|
* `PrReview.makeFanOut` provide the state-selection effects. A fingerprint is
|
|
94
94
|
* skip authority only when a profile fingerprint and authenticated review
|
|
95
|
-
* state bind it to settled assurance
|
|
96
|
-
* remains source-compatible but deliberately runs instead of claiming green
|
|
97
|
-
* assurance from unauthenticated prior-review text.
|
|
95
|
+
* state bind it to settled assurance.
|
|
98
96
|
*/
|
|
99
|
-
interface
|
|
100
|
-
|
|
97
|
+
interface HarnessedReviewerBase<E, R> {
|
|
98
|
+
/** The action composition root always supplies the selected run context. */
|
|
99
|
+
readonly run: (runOptions?: RunReviewOptions) => Effect.Effect<ReviewRunOutcome, E, R | ReviewExecutionContext>;
|
|
100
|
+
}
|
|
101
|
+
type HarnessedReviewer<E, R, FingerprintE, FingerprintR> = HarnessedReviewerBase<E, R> & ({
|
|
102
|
+
readonly fingerprint?: undefined;
|
|
103
|
+
readonly profileFingerprint?: undefined;
|
|
104
|
+
readonly snapshot?: undefined;
|
|
105
|
+
readonly filterFiles?: undefined;
|
|
106
|
+
} | {
|
|
101
107
|
/** Current effective changeset fingerprint; not standalone skip authority. */
|
|
102
|
-
readonly fingerprint
|
|
103
|
-
readonly profileFingerprint
|
|
104
|
-
readonly snapshot
|
|
108
|
+
readonly fingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
|
|
109
|
+
readonly profileFingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
|
|
110
|
+
readonly snapshot: Effect.Effect<{
|
|
105
111
|
readonly metadata: PullRequestMetadata;
|
|
106
112
|
readonly files: ReadonlyArray<ChangedFile>;
|
|
107
|
-
}, FingerprintE, FingerprintR
|
|
113
|
+
}, FingerprintE, FingerprintR>;
|
|
108
114
|
readonly filterFiles?: ((files: ReadonlyArray<ChangedFile>) => ReadonlyArray<ChangedFile>) | undefined;
|
|
109
|
-
}
|
|
115
|
+
});
|
|
110
116
|
/**
|
|
111
117
|
* Host-derived check conclusion; model verdict prose cannot weaken it.
|
|
112
118
|
*
|
|
113
119
|
* Blocking code findings outrank machinery gaps — they are the actionable
|
|
114
|
-
* signal. A machinery gap (
|
|
115
|
-
* `incomplete` with reasons that explicitly say the failure is
|
|
116
|
-
* uncertainty carried forward for retry, never an invitation to
|
|
117
|
-
*
|
|
120
|
+
* signal. A retryable machinery gap (failed passes, capacity overflow)
|
|
121
|
+
* concludes `incomplete` with reasons that explicitly say the failure is
|
|
122
|
+
* reviewer-side uncertainty carried forward for retry, never an invitation to
|
|
123
|
+
* change code. Undiffable files are the deliberate exception: no retry can
|
|
124
|
+
* settle them, so their reason instructs removal or ignore globs instead of
|
|
125
|
+
* promising an automatic retry. The flat reviewer's constant `unverified`
|
|
126
|
+
* assurance is not a gap.
|
|
118
127
|
*/
|
|
119
128
|
declare const concludeReviewOutcome: (outcome: ReviewRunOutcome) => {
|
|
120
129
|
readonly conclusion: ReviewCheckConclusion;
|
|
@@ -154,7 +163,7 @@ declare const runReviewAction: <E, R, FingerprintE = never, FingerprintR = never
|
|
|
154
163
|
} | {
|
|
155
164
|
_tag: "Completed";
|
|
156
165
|
outcome: ReviewRunOutcome;
|
|
157
|
-
}, E | FingerprintE | Config.ConfigError | import("effect/PlatformError").PlatformError | PullRequestSourceFailure | ReviewGateFailed | ReviewTargetUnresolved, FileSystem.FileSystem | import("effect/unstable/http/HttpClient").HttpClient | ReviewStateAuthenticator | Exclude<FingerprintR, PriorReviews | PullRequestSource | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost> | Exclude<R, PriorReviews | PullRequestSource | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost>>;
|
|
166
|
+
}, E | FingerprintE | Config.ConfigError | import("effect/PlatformError").PlatformError | PullRequestSourceFailure | ReviewGateFailed | ReviewTargetUnresolved, FileSystem.FileSystem | import("effect/unstable/http/HttpClient").HttpClient | ReviewStateAuthenticator | Exclude<FingerprintR, PriorReviews | PullRequestSource | ReviewAdjudicationHost | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost> | Exclude<Exclude<R, PullRequestSource | ReviewExecutionContext>, PriorReviews | PullRequestSource | ReviewAdjudicationHost | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost>>;
|
|
158
167
|
/**
|
|
159
168
|
* The packaged, environment-driven action program: inputs from PR_REVIEW_*,
|
|
160
169
|
* reviewer built from the packaged factory, provider client from the
|
package/dist/action.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as reviewBudgetLimits, C as PrReview, H as InvalidEffortInput, W as parseEffortPosition, a as anthropicClientLayer, c as makeOpenAiReviewModel, f as gitHubReviewLayers, g as ReviewProgressReporter, k as fanOutReviewBudgetLimits, l as openAiClientLayer, m as resolveReviewTarget, n as DEFAULT_PROVIDER, o as describeReviewModel, p as readGitHubEvent, s as makeAnthropicReviewModel } from "./providers-
|
|
1
|
+
import { An as validateReviewState, Cn as fullReviewSelection, En as selectedPullRequestSourceLayer, St as splitCarriedScope, Tn as selectReviewRange, Yt as retireStaleReviews, a as PriorReviews, cn as ReviewStateAuthenticator, gn as adjudicationIdentity, jn as webCryptoReviewStateAuthenticatorLayer, jt as collectReviewAdjudications, kn as unavailableReviewStateAuthenticatorLayer, nn as ReviewExecutionContext, rn as ReviewHeadComparison, vn as concernIdentity, vr as PullRequestSource, wn as isLineageAncestor, xr as normalizeRepoRelativePath, yn as findingIdentity } from "./github-NjgxGqwM.mjs";
|
|
2
|
+
import { A as reviewBudgetLimits, C as PrReview, H as InvalidEffortInput, W as parseEffortPosition, a as anthropicClientLayer, c as makeOpenAiReviewModel, f as gitHubReviewLayers, g as ReviewProgressReporter, k as fanOutReviewBudgetLimits, l as openAiClientLayer, m as resolveReviewTarget, n as DEFAULT_PROVIDER, o as describeReviewModel, p as readGitHubEvent, s as makeAnthropicReviewModel } from "./providers-CODZQCmL.mjs";
|
|
3
3
|
import { n as compactReviewLoggingLayer } from "./logging-Q4j0oub-.mjs";
|
|
4
4
|
import { Config, Console, Effect, FileSystem, Layer, Option, Redacted, Schema } from "effect";
|
|
5
5
|
import { BudgetExceeded, UsageBudgetLimits } from "effect-agent";
|
|
@@ -127,7 +127,6 @@ const outcomeOutputs = (outcome, conclusion) => [
|
|
|
127
127
|
["verdict", outcome.review.verdict],
|
|
128
128
|
["input-coverage", outcome.inputCoverage.status],
|
|
129
129
|
["review-assurance", outcome.assurance.status],
|
|
130
|
-
["coverage", outcome.coverage.status],
|
|
131
130
|
["review-mode", outcome.reviewMode ?? "full"],
|
|
132
131
|
["review-reason", outcome.reviewReason ?? "direct full review"],
|
|
133
132
|
["inline-comments", String(outcome.plan.comments.length)],
|
|
@@ -137,18 +136,22 @@ const outcomeOutputs = (outcome, conclusion) => [
|
|
|
137
136
|
...outcome.usage === void 0 ? [] : [["input-tokens", String(outcome.usage.inputTokens)], ["output-tokens", String(outcome.usage.outputTokens)]],
|
|
138
137
|
["review-url", outcome.published?.url ?? ""]
|
|
139
138
|
];
|
|
140
|
-
const outcomeSummary = (outcome, modelLabel, conclusion) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
]
|
|
139
|
+
const outcomeSummary = (outcome, modelLabel, conclusion) => {
|
|
140
|
+
const scope = splitCarriedScope(outcome);
|
|
141
|
+
return [
|
|
142
|
+
"### Pull-request review",
|
|
143
|
+
`- Check conclusion: **${conclusion}**`,
|
|
144
|
+
`- Verdict: **${outcome.review.verdict}**`,
|
|
145
|
+
`- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
|
|
146
|
+
`- Review assurance: **${outcome.assurance.status}** · general discovery ${outcome.assurance.completedGeneralDiscoveryPasses}/${outcome.assurance.requiredGeneralDiscoveryPasses} · specialist ${outcome.assurance.completedSpecialistPasses}/${outcome.assurance.requiredSpecialistPasses} · verification ${outcome.assurance.completedVerificationPasses}/${outcome.assurance.requiredVerificationPasses}`,
|
|
147
|
+
`- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
|
|
148
|
+
...scope.retryablePaths.length === 0 ? [] : [`- Carried forward: ${scope.retryablePaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`],
|
|
149
|
+
...scope.undiffablePaths.length === 0 ? [] : [`- Unreviewable: ${scope.undiffablePaths.length} path(s) with no reviewable diff (binary or oversized) — remove them from the pull request or exclude them with ignore globs`],
|
|
150
|
+
...modelLabel === void 0 ? [] : [`- Model: \`${modelLabel}\``],
|
|
151
|
+
...outcome.usage === void 0 ? [] : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`],
|
|
152
|
+
...outcome.published === void 0 ? ["- Dry run: nothing posted"] : [`- Posted: ${outcome.published.url}`]
|
|
153
|
+
];
|
|
154
|
+
};
|
|
152
155
|
const skip = (reason) => Effect.gen(function* () {
|
|
153
156
|
yield* Console.log(`Skipping review: ${reason}`);
|
|
154
157
|
yield* writeActionOutputs([["skipped", "true"], ["skip-reason", reason]]);
|
|
@@ -170,15 +173,20 @@ const blockingReasons = (input) => [...input.findings.filter((finding) => findin
|
|
|
170
173
|
* Host-derived check conclusion; model verdict prose cannot weaken it.
|
|
171
174
|
*
|
|
172
175
|
* Blocking code findings outrank machinery gaps — they are the actionable
|
|
173
|
-
* signal. A machinery gap (
|
|
174
|
-
* `incomplete` with reasons that explicitly say the failure is
|
|
175
|
-
* uncertainty carried forward for retry, never an invitation to
|
|
176
|
-
*
|
|
176
|
+
* signal. A retryable machinery gap (failed passes, capacity overflow)
|
|
177
|
+
* concludes `incomplete` with reasons that explicitly say the failure is
|
|
178
|
+
* reviewer-side uncertainty carried forward for retry, never an invitation to
|
|
179
|
+
* change code. Undiffable files are the deliberate exception: no retry can
|
|
180
|
+
* settle them, so their reason instructs removal or ignore globs instead of
|
|
181
|
+
* promising an automatic retry. The flat reviewer's constant `unverified`
|
|
182
|
+
* assurance is not a gap.
|
|
177
183
|
*/
|
|
178
184
|
const concludeReviewOutcome = (outcome) => {
|
|
179
185
|
const machinery = [];
|
|
180
186
|
if (outcome.inputCoverage.status === "incomplete" || outcome.assurance.status === "incomplete") {
|
|
181
|
-
|
|
187
|
+
const scope = splitCarriedScope(outcome);
|
|
188
|
+
if (scope.retryableGap) machinery.push(scope.retryablePaths.length > 0 ? `review infrastructure did not settle — a reviewer-side gap, not a code defect; ${scope.retryablePaths.length} path(s) are carried forward and retried automatically on the next run` : "review infrastructure did not settle — a reviewer-side gap, not a code defect");
|
|
189
|
+
if (scope.undiffablePaths.length > 0) machinery.push(`${scope.undiffablePaths.length} path(s) have no reviewable diff (binary or oversized) and no retry can settle them — remove them from the pull request or exclude them with ignore globs`);
|
|
182
190
|
if (outcome.inputCoverage.status === "incomplete") machinery.push(...outcome.inputCoverage.reasons);
|
|
183
191
|
if (outcome.assurance.status === "incomplete") machinery.push(...outcome.assurance.reasons);
|
|
184
192
|
}
|
|
@@ -199,10 +207,10 @@ const concludeReviewOutcome = (outcome) => {
|
|
|
199
207
|
reasons: []
|
|
200
208
|
};
|
|
201
209
|
};
|
|
202
|
-
const concludeReviewState = (state) => {
|
|
210
|
+
const concludeReviewState = (state, adjudicated) => {
|
|
203
211
|
const reasons = blockingReasons({
|
|
204
|
-
findings: state.unresolvedFindings,
|
|
205
|
-
concerns: state.unresolvedConcerns
|
|
212
|
+
findings: state.unresolvedFindings.filter((finding) => !adjudicated.has(findingIdentity(finding))),
|
|
213
|
+
concerns: state.unresolvedConcerns.filter((concern) => !adjudicated.has(concernIdentity(concern)))
|
|
206
214
|
});
|
|
207
215
|
return reasons.length > 0 ? {
|
|
208
216
|
conclusion: "blocking",
|
|
@@ -213,16 +221,15 @@ const concludeReviewState = (state) => {
|
|
|
213
221
|
};
|
|
214
222
|
};
|
|
215
223
|
const skipCoveredReview = Effect.fn("skipCoveredReview")(function* (input) {
|
|
216
|
-
const
|
|
224
|
+
const adjudications = yield* collectReviewAdjudications(input.state.adjudications ?? []);
|
|
225
|
+
const result = concludeReviewState(input.state, new Set(adjudications.map(adjudicationIdentity)));
|
|
217
226
|
yield* Console.log(`Skipping review of ${input.repository}#${input.pullRequestNumber}: ${input.reason}.`);
|
|
218
227
|
yield* writeActionOutputs([
|
|
219
228
|
["skipped", "true"],
|
|
220
229
|
["skip-reason", input.reason],
|
|
221
|
-
...input.fingerprint === void 0 ? [] : [["fingerprint", input.fingerprint]],
|
|
222
230
|
["conclusion", result.conclusion],
|
|
223
231
|
["input-coverage", "complete"],
|
|
224
232
|
["review-assurance", "settled"],
|
|
225
|
-
["coverage", "complete"],
|
|
226
233
|
["review-mode", "incremental"]
|
|
227
234
|
]);
|
|
228
235
|
yield* writeStepSummary([
|
|
@@ -258,14 +265,10 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
|
|
|
258
265
|
return yield* Effect.gen(function* () {
|
|
259
266
|
let selection;
|
|
260
267
|
if (reviewer.profileFingerprint !== void 0) {
|
|
261
|
-
const source = yield* PullRequestSource;
|
|
262
268
|
const [snapshot, profileFingerprint, currentFingerprint] = yield* Effect.all([
|
|
263
|
-
reviewer.snapshot
|
|
264
|
-
metadata: source.metadata,
|
|
265
|
-
files: source.anchorFiles
|
|
266
|
-
}),
|
|
269
|
+
reviewer.snapshot,
|
|
267
270
|
reviewer.profileFingerprint,
|
|
268
|
-
reviewer.fingerprint
|
|
271
|
+
reviewer.fingerprint.pipe(Effect.map((fingerprint) => fingerprint), Effect.orElseSucceed(() => void 0))
|
|
269
272
|
]);
|
|
270
273
|
const { metadata, files: fullFiles } = snapshot;
|
|
271
274
|
const history = options.priorReviews ?? (yield* PriorReviews);
|
|
@@ -283,13 +286,12 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
|
|
|
283
286
|
failure: void 0
|
|
284
287
|
})
|
|
285
288
|
}));
|
|
286
|
-
const equivalentPatchState = (options.reviewMode ?? "incremental") === "incremental" && options.skipUnchanged !== false && recovered.state !== void 0 && recovered.state.settled && currentFingerprint !== void 0 && validateReviewState(recovered.state, metadata, profileFingerprint) === void 0 && recovered.state.
|
|
289
|
+
const equivalentPatchState = (options.reviewMode ?? "incremental") === "incremental" && options.skipUnchanged !== false && recovered.state !== void 0 && recovered.state.settled && currentFingerprint !== void 0 && validateReviewState(recovered.state, metadata, profileFingerprint) === void 0 && recovered.state.settledScopeFingerprint === currentFingerprint ? recovered.state : void 0;
|
|
287
290
|
if (equivalentPatchState !== void 0) return yield* skipCoveredReview({
|
|
288
291
|
repository: target.repository,
|
|
289
292
|
pullRequestNumber: target.number,
|
|
290
293
|
reason: equivalentPatchState.reviewedHeadSha === metadata.headSha ? "the current head already has settled stored review assurance" : "the effective pull-request patch is unchanged since the last settled review",
|
|
291
|
-
state: equivalentPatchState
|
|
292
|
-
fingerprint: currentFingerprint
|
|
294
|
+
state: equivalentPatchState
|
|
293
295
|
});
|
|
294
296
|
let comparison;
|
|
295
297
|
let baseComparison;
|
|
@@ -346,6 +348,15 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
|
|
|
346
348
|
state: selection.priorState
|
|
347
349
|
});
|
|
348
350
|
}
|
|
351
|
+
const executionContext = selection ?? (yield* Effect.gen(function* () {
|
|
352
|
+
const source = yield* PullRequestSource;
|
|
353
|
+
const [metadata, files] = yield* Effect.all([source.metadata, source.changedFiles]);
|
|
354
|
+
return fullReviewSelection({
|
|
355
|
+
reason: "explicit custom-reviewer full review without continuity selection",
|
|
356
|
+
files,
|
|
357
|
+
totalFiles: metadata.totalChangedFiles
|
|
358
|
+
});
|
|
359
|
+
}));
|
|
349
360
|
yield* Console.log(`Reviewing ${target.repository}#${target.number} (${options.post === false ? "dry run" : "posting"})...`);
|
|
350
361
|
const runUrl = yield* resolveRunUrl();
|
|
351
362
|
const progress = options.progressComment === true && options.post !== false ? Option.some(yield* ReviewProgressReporter) : Option.none();
|
|
@@ -353,9 +364,9 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
|
|
|
353
364
|
const headMetadata = yield* (yield* PullRequestSource).metadata.pipe(Effect.orElseSucceed(() => void 0));
|
|
354
365
|
yield* progress.value.begin({
|
|
355
366
|
headSha: headMetadata?.headSha,
|
|
356
|
-
reviewMode:
|
|
357
|
-
reviewReason:
|
|
358
|
-
filesInScope:
|
|
367
|
+
reviewMode: executionContext.mode,
|
|
368
|
+
reviewReason: executionContext.reason,
|
|
369
|
+
filesInScope: executionContext.files.length,
|
|
359
370
|
modelLabel: options.modelLabel,
|
|
360
371
|
runUrl
|
|
361
372
|
});
|
|
@@ -369,7 +380,8 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
|
|
|
369
380
|
runUrl,
|
|
370
381
|
modelLabel: options.modelLabel
|
|
371
382
|
}))) : runReview;
|
|
372
|
-
const
|
|
383
|
+
const executionLayer = Layer.merge(Layer.succeed(ReviewExecutionContext)(executionContext), selectedPullRequestSourceLayer(executionContext));
|
|
384
|
+
const outcome = yield* reviewEffect.pipe(Effect.provide(executionLayer));
|
|
373
385
|
yield* Console.log(`Review finished in ${outcome.turns} turn(s): verdict ${outcome.review.verdict}, ${outcome.plan.comments.length} inline comment(s), ${outcome.plan.demoted.length} demoted finding(s).`);
|
|
374
386
|
if (outcome.published !== void 0) {
|
|
375
387
|
yield* Console.log(`Posted ${outcome.published.event} review: ${outcome.published.url}`);
|