@effect-agent/pr-review 0.1.0-beta.11 → 0.1.0-beta.111

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 (59) hide show
  1. package/NOTICE +26 -0
  2. package/README.md +181 -172
  3. package/dist/Review.d.mts +294 -0
  4. package/dist/Review.mjs +738 -0
  5. package/dist/Review.mjs.map +1 -0
  6. package/dist/ReviewRepository-Wd_4qCaO.d.mts +71 -0
  7. package/dist/ReviewRepository.d.mts +2 -0
  8. package/dist/ReviewRepository.mjs +15 -0
  9. package/dist/ReviewRepository.mjs.map +1 -0
  10. package/dist/index.d.mts +3 -718
  11. package/dist/index.mjs +3 -66
  12. package/dist/repository-D7NN3225.mjs +101 -0
  13. package/dist/repository-D7NN3225.mjs.map +1 -0
  14. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  15. package/package.json +1 -54
  16. package/src/Review.ts +1111 -0
  17. package/src/ReviewRepository.ts +9 -0
  18. package/src/index.ts +2 -21
  19. package/src/internal/repository.ts +156 -0
  20. package/dist/action.d.mts +0 -190
  21. package/dist/action.mjs +0 -423
  22. package/dist/action.mjs.map +0 -1
  23. package/dist/cli.d.mts +0 -1
  24. package/dist/cli.mjs +0 -103
  25. package/dist/cli.mjs.map +0 -1
  26. package/dist/fan-out-TrA9EUCr.d.mts +0 -1085
  27. package/dist/github-5TCFrxfX.mjs +0 -1676
  28. package/dist/github-5TCFrxfX.mjs.map +0 -1
  29. package/dist/index.mjs.map +0 -1
  30. package/dist/providers-DobNWMUn.mjs +0 -990
  31. package/dist/providers-DobNWMUn.mjs.map +0 -1
  32. package/dist/testing.d.mts +0 -131
  33. package/dist/testing.mjs +0 -230
  34. package/dist/testing.mjs.map +0 -1
  35. package/src/action.ts +0 -697
  36. package/src/cli.ts +0 -214
  37. package/src/internal/action-entry.ts +0 -43
  38. package/src/internal/coverage.ts +0 -245
  39. package/src/internal/diff.ts +0 -134
  40. package/src/internal/effort.ts +0 -86
  41. package/src/internal/factory.ts +0 -374
  42. package/src/internal/fan-out-scripted.ts +0 -164
  43. package/src/internal/fan-out.ts +0 -450
  44. package/src/internal/fingerprint.ts +0 -74
  45. package/src/internal/fixtures.ts +0 -131
  46. package/src/internal/github-env.ts +0 -148
  47. package/src/internal/github.ts +0 -774
  48. package/src/internal/ignore.ts +0 -88
  49. package/src/internal/profiles.ts +0 -79
  50. package/src/internal/providers.ts +0 -94
  51. package/src/internal/render.ts +0 -428
  52. package/src/internal/retirement.ts +0 -332
  53. package/src/internal/review-agent.ts +0 -385
  54. package/src/internal/review-state.ts +0 -488
  55. package/src/internal/review-units.ts +0 -167
  56. package/src/internal/run.ts +0 -397
  57. package/src/internal/scripted.ts +0 -108
  58. package/src/internal/source.ts +0 -110
  59. package/src/testing.ts +0 -8
@@ -1,88 +0,0 @@
1
- import { Effect, Layer } from "effect";
2
-
3
- import { PullRequestMetadata, PullRequestSource, ReviewInputViolation } from "./source.ts";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Configured ignore globs, applied at the source port. Ignored files are
7
- // removed from the reviewer's entire observation surface — the changeset
8
- // list, diffs, and head reads — so the model never spends budget on them and
9
- // can never anchor a finding to them. Filtering fails closed: reading an
10
- // ignored path is a ReviewInputViolation, exactly like a path outside the
11
- // changeset.
12
- // ---------------------------------------------------------------------------
13
-
14
- const REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
15
-
16
- // Placeholders for the directory-crossing wildcard while single-segment
17
- // wildcards are rewritten; NUL/SOH cannot appear in a valid repository path.
18
- const CROSSING_SLASH = "\u0000";
19
- const CROSSING = "\u0001";
20
-
21
- /**
22
- * The supported glob vocabulary is deliberately minimal: `**` crosses
23
- * directory separators, `*` and `?` stay within one path segment, everything
24
- * else is literal. Every string compiles — there is no invalid pattern.
25
- */
26
- const globToRegExpSource = (pattern: string): string =>
27
- pattern
28
- .replace(REGEX_SPECIALS, String.raw`\$&`)
29
- .replaceAll("**/", CROSSING_SLASH)
30
- .replaceAll("**", CROSSING)
31
- .replaceAll("*", "[^/]*")
32
- .replaceAll("?", "[^/]")
33
- .replaceAll(CROSSING_SLASH, "(?:.*/)?")
34
- .replaceAll(CROSSING, ".*");
35
-
36
- /** Compile ignore globs into one predicate over repository-relative paths. */
37
- export const compileIgnoreGlobs = (
38
- patterns: ReadonlyArray<string>,
39
- ): ((path: string) => boolean) => {
40
- if (patterns.length === 0) return () => false;
41
- const expressions = patterns.map((pattern) => new RegExp(`^(?:${globToRegExpSource(pattern)})$`));
42
- return (path) => expressions.some((expression) => expression.test(path));
43
- };
44
-
45
- /**
46
- * Decorate the ambient PullRequestSource with configured ignore globs. The
47
- * resulting Layer requires the undecorated source, so callers provide their
48
- * real adapter beneath it. Metadata's changed-file total is reduced by the
49
- * ignored count: from the reviewer's perspective the ignored files do not
50
- * exist, and truncation reporting stays about the reviewer's own bound.
51
- */
52
- export const ignoringPullRequestSourceLayer = (
53
- patterns: ReadonlyArray<string>,
54
- ): Layer.Layer<PullRequestSource, never, PullRequestSource> =>
55
- Layer.effect(PullRequestSource)(
56
- Effect.gen(function* () {
57
- const source = yield* PullRequestSource;
58
- const ignored = compileIgnoreGlobs(patterns);
59
- const changedFiles = source.changedFiles.pipe(
60
- Effect.map((files) => files.filter((file) => !ignored(file.path))),
61
- );
62
- const anchorFiles = source.anchorFiles.pipe(
63
- Effect.map((files) => files.filter((file) => !ignored(file.path))),
64
- );
65
- const metadata = Effect.gen(function* () {
66
- const [meta, files] = yield* Effect.all([source.metadata, source.anchorFiles]);
67
- const ignoredCount = files.filter((file) => ignored(file.path)).length;
68
- return PullRequestMetadata.make({
69
- ...meta,
70
- totalChangedFiles: Math.max(0, meta.totalChangedFiles - ignoredCount),
71
- });
72
- });
73
- return PullRequestSource.of({
74
- metadata,
75
- changedFiles,
76
- anchorFiles,
77
- readFile: (path) =>
78
- ignored(path)
79
- ? Effect.fail(
80
- ReviewInputViolation.make({
81
- input: path,
82
- reason: "Path is excluded from this review by configuration.",
83
- }),
84
- )
85
- : source.readFile(path),
86
- });
87
- }),
88
- );
@@ -1,79 +0,0 @@
1
- import { Schema } from "effect";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Committed capability claims, schema-first: what this package's reviewers
5
- // promise and — just as deliberately — what they never claim. Deployment
6
- // class E (ephemeral): one bounded AgentRuntime.run per invocation, no
7
- // durability, no exactly-once external effects (DUR-003).
8
- // ---------------------------------------------------------------------------
9
-
10
- /** The flat reviewer's committed capability claim. */
11
- export class PullRequestReviewerProfile extends Schema.Class<PullRequestReviewerProfile>(
12
- "@effect-agent/pr-review/PullRequestReviewerProfile",
13
- )({
14
- /** Ephemeral runtime: one bounded AgentRuntime.run per invocation. */
15
- deploymentClass: Schema.Literal("E"),
16
- /** Every model-callable tool is a read of the pull request; none mutate. */
17
- readOnlyToolSurface: Schema.Literal(true),
18
- /** The review is posted by the host AFTER the run settles, never by a tool. */
19
- publicationOutsideAgentLoop: Schema.Literal(true),
20
- /** Finding anchors are validated against the parsed diff before posting. */
21
- anchorsValidatedBeforePublication: Schema.Literal(true),
22
- /** The live profile is env-gated out of every ordinary test gate. */
23
- liveProfileOptIn: Schema.Literal(true),
24
- /** Never claimed at any phase (DUR-003). */
25
- exactlyOnceExternalEffects: Schema.Literal(false),
26
- }) {}
27
-
28
- export const pullRequestReviewerProfile = PullRequestReviewerProfile.make({
29
- deploymentClass: "E",
30
- readOnlyToolSurface: true,
31
- publicationOutsideAgentLoop: true,
32
- anchorsValidatedBeforePublication: true,
33
- liveProfileOptIn: true,
34
- exactlyOnceExternalEffects: false,
35
- });
36
-
37
- /** The fan-out reviewer's committed capability claim, schema-first. */
38
- export class FanOutReviewerProfile extends Schema.Class<FanOutReviewerProfile>(
39
- "@effect-agent/pr-review/FanOutReviewerProfile",
40
- )({
41
- /** Ephemeral runtime: one bounded AgentRuntime.run per invocation. */
42
- deploymentClass: Schema.Literal("E"),
43
- /** Every model-callable tool — parent and child — is a read; none mutate. */
44
- readOnlyToolSurface: Schema.Literal(true),
45
- /** The review is posted by the host AFTER the run settles, never by a tool. */
46
- publicationOutsideAgentLoop: Schema.Literal(true),
47
- /** Child findings are untrusted; anchors are validated against the parsed diff. */
48
- anchorsValidatedBeforePublication: Schema.Literal(true),
49
- /** S1 attached ephemeral delegation at depth 1; nested delegation is rejected. */
50
- attachedEphemeralDelegation: Schema.Literal(true),
51
- /** A failed unit surfaces to the coordinator as a typed failed result, never retried. */
52
- failedUnitsReportedNotRetried: Schema.Literal(true),
53
- /** The live profile is env-gated out of every ordinary test gate. */
54
- liveProfileOptIn: Schema.Literal(true),
55
- /** Never claimed at any phase (DUR-003). */
56
- exactlyOnceExternalEffects: Schema.Literal(false),
57
- }) {}
58
-
59
- export const fanOutReviewerProfile = FanOutReviewerProfile.make({
60
- deploymentClass: "E",
61
- readOnlyToolSurface: true,
62
- publicationOutsideAgentLoop: true,
63
- anchorsValidatedBeforePublication: true,
64
- attachedEphemeralDelegation: true,
65
- failedUnitsReportedNotRetried: true,
66
- liveProfileOptIn: true,
67
- exactlyOnceExternalEffects: false,
68
- });
69
-
70
- export const LIVE_GATE_ENV = "EFFECT_AGENT_LIVE";
71
-
72
- /**
73
- * `EFFECT_AGENT_LIVE=1` plus the named credential is the only enabling
74
- * combination for live (network, billed) profiles.
75
- */
76
- export const liveProfileEnabled = (
77
- env: Record<string, string | undefined>,
78
- credentialEnv: string,
79
- ): boolean => env[LIVE_GATE_ENV] === "1" && (env[credentialEnv] ?? "") !== "";
@@ -1,94 +0,0 @@
1
- import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
2
- import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
3
- import { Config, Layer } from "effect";
4
- import { FetchHttpClient } from "effect/unstable/http";
5
-
6
- import { resolveEffortRung, type EffortAliasName, type EffortPosition } from "./effort.ts";
7
-
8
- // ---------------------------------------------------------------------------
9
- // Built-in provider bindings for the two host entrypoints (CLI and Action).
10
- // The library itself stays provider-agnostic — the configuration factory
11
- // takes any Effect AI Model — and these helpers exist so the batteries-
12
- // included paths need one flag and one credential, nothing more. Client
13
- // Layers carry their redacted credentials from configuration; the
14
- // application supplies them at the edge (D-027).
15
- // ---------------------------------------------------------------------------
16
-
17
- export type ReviewProvider = "openai" | "anthropic";
18
-
19
- export const DEFAULT_PROVIDER: ReviewProvider = "openai";
20
-
21
- export const DEFAULT_MODEL: Record<ReviewProvider, string> = {
22
- openai: "gpt-5.6-sol",
23
- anthropic: "claude-sonnet-5",
24
- };
25
-
26
- export const PROVIDER_CREDENTIAL_ENV: Record<ReviewProvider, string> = {
27
- openai: "OPENAI_API_KEY",
28
- anthropic: "ANTHROPIC_API_KEY",
29
- };
30
-
31
- /**
32
- * Each provider's offered reasoning-effort ladder, cheapest first. The rungs
33
- * that turn reasoning off (`none`, `minimal`) are deliberately not offered —
34
- * no review run wants them. An `EffortPosition` resolves into the running
35
- * provider's own ladder, so the same stored position survives a provider or
36
- * model change.
37
- */
38
- export const PROVIDER_EFFORT_RUNGS = {
39
- openai: ["low", "medium", "high", "xhigh"],
40
- anthropic: ["low", "medium", "high"],
41
- } as const satisfies Record<
42
- ReviewProvider,
43
- readonly [EffortAliasName, ...ReadonlyArray<EffortAliasName>]
44
- >;
45
-
46
- /** One OpenAI review model binding with the package's structured-output settings. */
47
- export const makeOpenAiReviewModel = (model?: string, effort?: EffortPosition) =>
48
- OpenAiLanguageModel.model(model ?? DEFAULT_MODEL.openai, {
49
- // OpenAI counts hidden reasoning tokens and visible answer tokens against
50
- // this same ceiling. High-effort reviews can exhaust an 8k allowance
51
- // after reading every file but before emitting their structured report.
52
- max_output_tokens: 32_000,
53
- store: false,
54
- strictJsonSchema: true,
55
- ...(effort === undefined
56
- ? {}
57
- : { reasoning: { effort: resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS.openai) } }),
58
- });
59
-
60
- /** One Anthropic review model binding with the package's output settings. */
61
- export const makeAnthropicReviewModel = (model?: string, effort?: EffortPosition) =>
62
- AnthropicLanguageModel.model(model ?? DEFAULT_MODEL.anthropic, {
63
- max_tokens: 8_000,
64
- ...(effort === undefined
65
- ? {}
66
- : { output_config: { effort: resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS.anthropic) } }),
67
- });
68
-
69
- /**
70
- * The human-readable descriptor of one provider binding, e.g.
71
- * `openai/gpt-5.6-sol (effort high)`. Rendered into the review footer and
72
- * included in the changeset-fingerprint signature, so a provider, model, or
73
- * effort change re-reviews instead of skipping.
74
- */
75
- export const describeReviewModel = (
76
- provider: ReviewProvider,
77
- model?: string,
78
- effort?: EffortPosition,
79
- ): string => {
80
- const base = `${provider}/${model ?? DEFAULT_MODEL[provider]}`;
81
- return effort === undefined
82
- ? base
83
- : `${base} (effort ${resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS[provider])})`;
84
- };
85
-
86
- /** The OpenAI client Layer, credential from `OPENAI_API_KEY`. */
87
- export const openAiClientLayer = OpenAiClient.layerConfig({
88
- apiKey: Config.redacted(PROVIDER_CREDENTIAL_ENV.openai),
89
- }).pipe(Layer.provide(FetchHttpClient.layer));
90
-
91
- /** The Anthropic client Layer, credential from `ANTHROPIC_API_KEY`. */
92
- export const anthropicClientLayer = AnthropicClient.layerConfig({
93
- apiKey: Config.redacted(PROVIDER_CREDENTIAL_ENV.anthropic),
94
- }).pipe(Layer.provide(FetchHttpClient.layer));