@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Cn as ChangedFile, Rt as CodeReview, Sn as normalizeRepoRelativePath, _n as MAX_FILE_CHARS, a as PriorReviews, i as PriorReviewLookupFailure, o as PublishedReview, rt as FileReviewReport, s as ReviewPublisher, vn as PullRequestMetadata, xn as ReviewInputViolation, yn as PullRequestSource } from "./github-DnenG3be.mjs";
1
+ import { $n as normalizeRepoRelativePath, Jn as MAX_FILE_CHARS, Qn as ReviewInputViolation, Xn as PullRequestSource, Yn as PullRequestMetadata, a as PriorReviews, ct as FileReviewReport, fn as CodeReview, i as PriorReviewLookupFailure, lt as FileReviewRequest, o as PublishedReview, s as ReviewPublisher, tr as ChangedFile } from "./github-BgtP7Rdv.mjs";
2
2
  import { DateTime, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
3
3
  import { LanguageModel, Model } from "effect/unstable/ai";
4
4
  //#region src/internal/scripted.ts
@@ -89,28 +89,32 @@ const makeOfflineReviewerModel = (script) => makePromptKeyedModel("pr-review-off
89
89
  //#endregion
90
90
  //#region src/internal/fan-out-scripted.ts
91
91
  const OFFLINE_UNITS_CALL_ID = "units-1";
92
- /** The delegation Tool Call id the scripted coordinator uses for one unit. */
93
- const offlineUnitCallId = (unitId) => `delegate-${unitId}`;
94
- /** The diff Tool Call id the scripted child uses for one unit. */
95
- const offlineChildDiffCallId = (unitId) => `fanout-diff-${unitId}`;
96
- /**
97
- * Build the offline scripted coordinator model. Turn 1 lists the review
98
- * units, Turn 2 declares one delegation Tool Call per scripted unit in one
99
- * batch, Turn 3 returns the scripted merged review JSON. Decisions key on
100
- * tool-call ids already committed to the prompt.
101
- */
92
+ const offlineUnitCallId = (workId) => `delegate-${workId}`;
93
+ const scriptedUnitCallId = (calls, index) => {
94
+ const call = calls[index];
95
+ if (call === void 0) return "delegate-none";
96
+ const occurrence = calls.slice(0, index + 1).filter((candidate) => candidate.workId === call.workId).length;
97
+ const base = offlineUnitCallId(call.workId);
98
+ return occurrence === 1 ? base : `${base}-${occurrence}`;
99
+ };
102
100
  const makeOfflineFanOutCoordinatorModel = (script) => {
103
- const firstUnitCallId = offlineUnitCallId(script.unitCalls[0]?.unitId ?? "unit-none");
101
+ const firstDiscovery = scriptedUnitCallId(script.discoveryCalls, 0);
102
+ const firstVerification = scriptedUnitCallId(script.verificationCalls, 0);
104
103
  return makePromptKeyedModel("pr-fanout-coordinator-offline", (promptJson) => {
105
- if (promptJson.includes(firstUnitCallId)) return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
106
- if (promptJson.includes("units-1")) return scriptedToolTurn(...script.unitCalls.map((unit) => ({
104
+ if (script.discoveryCalls.length === 0 && promptJson.includes("units-1")) return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
105
+ if (script.verificationCalls.length === 0 ? promptJson.includes(firstDiscovery) : promptJson.includes(firstVerification)) return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
106
+ if (promptJson.includes(firstDiscovery)) return scriptedToolTurn(...script.verificationCalls.map((call, index) => ({
107
+ type: "tool-call",
108
+ id: scriptedUnitCallId(script.verificationCalls, index),
109
+ name: "delegate_file_review",
110
+ params: Schema.encodeSync(FileReviewRequest)(call),
111
+ providerExecuted: false
112
+ })));
113
+ if (promptJson.includes("units-1")) return scriptedToolTurn(...script.discoveryCalls.map((call, index) => ({
107
114
  type: "tool-call",
108
- id: offlineUnitCallId(unit.unitId),
115
+ id: scriptedUnitCallId(script.discoveryCalls, index),
109
116
  name: "delegate_file_review",
110
- params: {
111
- unitId: unit.unitId,
112
- paths: unit.paths
113
- },
117
+ params: Schema.encodeSync(FileReviewRequest)(call),
114
118
  providerExecuted: false
115
119
  })));
116
120
  return scriptedToolTurn({
@@ -122,41 +126,9 @@ const makeOfflineFanOutCoordinatorModel = (script) => {
122
126
  });
123
127
  });
124
128
  };
125
- /**
126
- * Build the offline scripted file-reviewer model shared by every delegated
127
- * child. Each child Run builds the Model Layer inside its own scope; the
128
- * script entry is selected by the briefed unitId present in the child's OWN
129
- * prompt, and the turn is selected by whether that unit's diff Tool Call id
130
- * is already committed there — content-keyed on both axes, so concurrent
131
- * children never interfere. First-turn child prompts are recorded for
132
- * context-isolation assertions.
133
- */
134
129
  const makeOfflineFileReviewerModel = (scripts) => Effect.gen(function* () {
135
130
  const calls = yield* Ref.make(0);
136
131
  const prompts = yield* Ref.make([]);
137
- const decide = (promptJson) => {
138
- const script = scripts.find((candidate) => promptJson.includes(candidate.unitId));
139
- if (script === void 0) return void 0;
140
- switch (script.outcome._tag) {
141
- case "budget-runaway": return scriptedToolTurn(...Array.from({ length: script.outcome.declaredCalls }, (_, index) => ({
142
- type: "tool-call",
143
- id: `runaway-${script.unitId}-${index + 1}`,
144
- name: "read_file_diff",
145
- params: { path: script.diffPath },
146
- providerExecuted: false
147
- })));
148
- case "malformed-output":
149
- case "findings":
150
- if (promptJson.includes(offlineChildDiffCallId(script.unitId))) return scriptedFinalParts(script.outcome._tag === "findings" ? JSON.stringify(Schema.encodeSync(FileReviewReport)(script.outcome.report)) : "this is not the JSON you are looking for");
151
- return scriptedToolTurn({
152
- type: "tool-call",
153
- id: offlineChildDiffCallId(script.unitId),
154
- name: "read_file_diff",
155
- params: { path: script.diffPath },
156
- providerExecuted: false
157
- });
158
- }
159
- };
160
132
  return {
161
133
  model: Model.make("scripted", "pr-fanout-file-reviewer-offline", Layer.effect(LanguageModel.LanguageModel, LanguageModel.make({
162
134
  generateText: () => Effect.succeed([]),
@@ -164,9 +136,11 @@ const makeOfflineFileReviewerModel = (scripts) => Effect.gen(function* () {
164
136
  yield* Ref.update(calls, (value) => value + 1);
165
137
  const promptJson = JSON.stringify(request.prompt);
166
138
  yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
167
- const parts = decide(promptJson);
168
- if (parts === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("The child prompt names no scripted review unit"));
169
- return Stream.fromIterable(parts);
139
+ const script = scripts.filter((candidate) => promptJson.includes(`for ${candidate.workId} in host-planned unit`));
140
+ if (script.length !== 1) return yield* Effect.die(/* @__PURE__ */ new Error("The child prompt must name exactly one scripted work ID"));
141
+ const [selected] = script;
142
+ if (selected === void 0) return yield* Effect.die("unreachable scripted match");
143
+ return Stream.fromIterable(scriptedFinalParts(selected.outcome._tag === "report" ? JSON.stringify(Schema.encodeSync(FileReviewReport)(selected.outcome.report)) : "this is not valid review JSON"));
170
144
  }))
171
145
  }))),
172
146
  calls: Ref.get(calls),
@@ -233,6 +207,6 @@ const staticPriorReviews = (fingerprint, options = {}) => PriorReviews.of({
233
207
  /** Layer form for consumers whose Effect explicitly requires `PriorReviews`. */
234
208
  const staticPriorReviewsLayer = (fingerprint, options = {}) => Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));
235
209
  //#endregion
236
- export { FixtureFile, FixturePullRequest, OFFLINE_DIFF_CALL_ID, OFFLINE_LIST_CALL_ID, OFFLINE_READ_CALL_ID, OFFLINE_UNITS_CALL_ID, SCRIPTED_TURN_USAGE, collectingReviewPublisherLayer, fixturePullRequestSourceLayer, makeOfflineFanOutCoordinatorModel, makeOfflineFileReviewerModel, makeOfflineReviewerModel, makePromptKeyedModel, offlineChildDiffCallId, offlineUnitCallId, scriptedFinalParts, scriptedToolTurn, staticPriorReviews, staticPriorReviewsLayer };
210
+ export { FixtureFile, FixturePullRequest, OFFLINE_DIFF_CALL_ID, OFFLINE_LIST_CALL_ID, OFFLINE_READ_CALL_ID, OFFLINE_UNITS_CALL_ID, SCRIPTED_TURN_USAGE, collectingReviewPublisherLayer, fixturePullRequestSourceLayer, makeOfflineFanOutCoordinatorModel, makeOfflineFileReviewerModel, makeOfflineReviewerModel, makePromptKeyedModel, offlineUnitCallId, scriptedFinalParts, scriptedToolTurn, staticPriorReviews, staticPriorReviewsLayer };
237
211
 
238
212
  //# sourceMappingURL=testing.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.mjs","names":[],"sources":["../src/internal/scripted.ts","../src/internal/fan-out-scripted.ts","../src/internal/fixtures.ts"],"sourcesContent":["import { Effect, Layer, Ref, Schema, Stream } from \"effect\";\nimport { LanguageModel, Model, type Response } from \"effect/unstable/ai\";\n\nimport { CodeReview } from \"./review-agent.ts\";\n\n// ---------------------------------------------------------------------------\n// Deterministic offline model for the flat reviewer: a prompt-aware scripted\n// model that walks the real tool surface — list, diff, read — then returns\n// the scripted review as its terminal JSON. Decisions key on committed\n// history in the prompt, never on call order, so replays stay honest.\n// ---------------------------------------------------------------------------\n\nexport const OFFLINE_LIST_CALL_ID = \"list-1\";\nexport const OFFLINE_DIFF_CALL_ID = \"diff-1\";\nexport const OFFLINE_READ_CALL_ID = \"read-1\";\n\n/** Usage attached to EVERY scripted model turn; tests pin exact aggregates. */\nexport const SCRIPTED_TURN_USAGE = { inputTokens: 64, outputTokens: 48 } as const;\n\nconst scriptedUsage = {\n inputTokens: { total: SCRIPTED_TURN_USAGE.inputTokens },\n outputTokens: { total: SCRIPTED_TURN_USAGE.outputTokens },\n};\n\nexport const scriptedToolTurn = (\n ...calls: ReadonlyArray<Response.StreamPartEncoded>\n): ReadonlyArray<Response.StreamPartEncoded> => [\n ...calls,\n { type: \"finish\", reason: \"tool-calls\", usage: scriptedUsage },\n];\n\nexport const scriptedFinalParts = (text: string): ReadonlyArray<Response.StreamPartEncoded> => [\n { type: \"text-start\", id: \"code-review\" },\n { type: \"text-delta\", id: \"code-review\", delta: text },\n { type: \"text-end\", id: \"code-review\" },\n { type: \"finish\", reason: \"stop\", usage: scriptedUsage },\n];\n\n/** A prompt-keyed scripted LanguageModel with call and prompt observability. */\nexport const makePromptKeyedModel = (\n name: string,\n decide: (promptJson: string) => ReadonlyArray<Response.StreamPartEncoded>,\n) =>\n Effect.gen(function* () {\n const calls = yield* Ref.make(0);\n const prompts = yield* Ref.make<ReadonlyArray<string>>([]);\n const model = Model.make(\n \"scripted\",\n name,\n Layer.effect(\n LanguageModel.LanguageModel,\n LanguageModel.make({\n generateText: () => Effect.succeed([]),\n streamText: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* Ref.update(calls, (value) => value + 1);\n const promptJson = JSON.stringify(request.prompt);\n yield* Ref.update(prompts, (previous) => [...previous, promptJson]);\n return Stream.fromIterable(decide(promptJson));\n }),\n ),\n }),\n ),\n );\n return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };\n });\n\n/**\n * Build the offline scripted reviewer model. Turn 1 lists the changeset,\n * Turn 2 reads one file diff, Turn 3 reads head context, Turn 4 returns the\n * scripted review JSON.\n */\nexport const makeOfflineReviewerModel = (script: {\n readonly diffPath: string;\n readonly readPath: string;\n readonly review: CodeReview;\n}) =>\n makePromptKeyedModel(\"pr-review-offline\", (promptJson) => {\n if (promptJson.includes(OFFLINE_READ_CALL_ID)) {\n return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));\n }\n if (promptJson.includes(OFFLINE_DIFF_CALL_ID)) {\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_READ_CALL_ID,\n name: \"read_file\",\n params: { path: script.readPath },\n providerExecuted: false,\n });\n }\n if (promptJson.includes(OFFLINE_LIST_CALL_ID)) {\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_DIFF_CALL_ID,\n name: \"read_file_diff\",\n params: { path: script.diffPath },\n providerExecuted: false,\n });\n }\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_LIST_CALL_ID,\n name: \"list_changed_files\",\n params: { scope: \"all\" },\n providerExecuted: false,\n });\n });\n","import { Effect, Layer, Ref, Schema, Stream } from \"effect\";\nimport { LanguageModel, Model, type Response } from \"effect/unstable/ai\";\n\nimport { FileReviewReport } from \"./fan-out.ts\";\nimport { CodeReview } from \"./review-agent.ts\";\nimport { makePromptKeyedModel, scriptedFinalParts, scriptedToolTurn } from \"./scripted.ts\";\n\n// ---------------------------------------------------------------------------\n// Deterministic offline models for the fan-out reviewer: prompt-keyed\n// scripted models for BOTH the coordinator and the file-reviewer children.\n// Both key every decision on committed history in the prompt (tool-call ids\n// and briefed unit ids), never on call order, so concurrent children and\n// replays stay honest.\n// ---------------------------------------------------------------------------\n\nexport const OFFLINE_UNITS_CALL_ID = \"units-1\";\n\n/** The delegation Tool Call id the scripted coordinator uses for one unit. */\nexport const offlineUnitCallId = (unitId: string): string => `delegate-${unitId}`;\n\n/** The diff Tool Call id the scripted child uses for one unit. */\nexport const offlineChildDiffCallId = (unitId: string): string => `fanout-diff-${unitId}`;\n\n/** One scripted delegation the offline coordinator declares. */\nexport interface OfflineUnitCall {\n readonly unitId: string;\n readonly paths: ReadonlyArray<string>;\n}\n\n/**\n * Build the offline scripted coordinator model. Turn 1 lists the review\n * units, Turn 2 declares one delegation Tool Call per scripted unit in one\n * batch, Turn 3 returns the scripted merged review JSON. Decisions key on\n * tool-call ids already committed to the prompt.\n */\nexport const makeOfflineFanOutCoordinatorModel = (script: {\n readonly unitCalls: ReadonlyArray<OfflineUnitCall>;\n readonly review: CodeReview;\n}) => {\n const firstUnitCallId = offlineUnitCallId(script.unitCalls[0]?.unitId ?? \"unit-none\");\n return makePromptKeyedModel(\"pr-fanout-coordinator-offline\", (promptJson) => {\n if (promptJson.includes(firstUnitCallId)) {\n return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));\n }\n if (promptJson.includes(OFFLINE_UNITS_CALL_ID)) {\n return scriptedToolTurn(\n ...script.unitCalls.map(\n (unit): Response.StreamPartEncoded => ({\n type: \"tool-call\",\n id: offlineUnitCallId(unit.unitId),\n name: \"delegate_file_review\",\n params: { unitId: unit.unitId, paths: unit.paths },\n providerExecuted: false,\n }),\n ),\n );\n }\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_UNITS_CALL_ID,\n name: \"list_review_units\",\n params: { scope: \"all\" },\n providerExecuted: false,\n });\n });\n};\n\n/** How one scripted child behaves for its briefed unit. */\nexport type OfflineUnitOutcome =\n /** Read one diff, then return the scripted report. */\n | { readonly _tag: \"findings\"; readonly report: FileReviewReport }\n /** Read one diff, then return non-JSON — the child fails typed (AgentOutputError). */\n | { readonly _tag: \"malformed-output\" }\n /**\n * Declare more Tool Calls than the child's AgentPolicy allows in one turn —\n * none executes and the child fails typed (AgentPolicyError \"tool-calls\",\n * the reviewer's deliberate `onExhaustion: \"fail\"` pin).\n */\n | { readonly _tag: \"budget-runaway\"; readonly declaredCalls: number };\n\nexport interface OfflineUnitScript {\n readonly unitId: string;\n /** The one file the scripted child reads the diff of. */\n readonly diffPath: string;\n readonly outcome: OfflineUnitOutcome;\n}\n\n/**\n * Build the offline scripted file-reviewer model shared by every delegated\n * child. Each child Run builds the Model Layer inside its own scope; the\n * script entry is selected by the briefed unitId present in the child's OWN\n * prompt, and the turn is selected by whether that unit's diff Tool Call id\n * is already committed there — content-keyed on both axes, so concurrent\n * children never interfere. First-turn child prompts are recorded for\n * context-isolation assertions.\n */\nexport const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitScript>) =>\n Effect.gen(function* () {\n const calls = yield* Ref.make(0);\n const prompts = yield* Ref.make<ReadonlyArray<string>>([]);\n const decide = (promptJson: string): ReadonlyArray<Response.StreamPartEncoded> | undefined => {\n const script = scripts.find((candidate) => promptJson.includes(candidate.unitId));\n if (script === undefined) return undefined;\n switch (script.outcome._tag) {\n case \"budget-runaway\": {\n return scriptedToolTurn(\n ...Array.from(\n { length: script.outcome.declaredCalls },\n (_, index): Response.StreamPartEncoded => ({\n type: \"tool-call\",\n id: `runaway-${script.unitId}-${index + 1}`,\n name: \"read_file_diff\",\n params: { path: script.diffPath },\n providerExecuted: false,\n }),\n ),\n );\n }\n case \"malformed-output\":\n case \"findings\": {\n if (promptJson.includes(offlineChildDiffCallId(script.unitId))) {\n return scriptedFinalParts(\n script.outcome._tag === \"findings\"\n ? JSON.stringify(Schema.encodeSync(FileReviewReport)(script.outcome.report))\n : \"this is not the JSON you are looking for\",\n );\n }\n return scriptedToolTurn({\n type: \"tool-call\",\n id: offlineChildDiffCallId(script.unitId),\n name: \"read_file_diff\",\n params: { path: script.diffPath },\n providerExecuted: false,\n });\n }\n }\n };\n const model = Model.make(\n \"scripted\",\n \"pr-fanout-file-reviewer-offline\",\n Layer.effect(\n LanguageModel.LanguageModel,\n LanguageModel.make({\n generateText: () => Effect.succeed([]),\n streamText: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* Ref.update(calls, (value) => value + 1);\n const promptJson = JSON.stringify(request.prompt);\n yield* Ref.update(prompts, (previous) => [...previous, promptJson]);\n const parts = decide(promptJson);\n if (parts === undefined) {\n return yield* Effect.die(\n new Error(\"The child prompt names no scripted review unit\"),\n );\n }\n return Stream.fromIterable(parts);\n }),\n ),\n }),\n ),\n );\n return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };\n });\n","import { DateTime, Effect, Layer, Option, Ref, Schema } from \"effect\";\n\nimport { ChangedFile } from \"./diff.ts\";\nimport {\n PriorReviewLookupFailure,\n PriorReviews,\n PublishedReview,\n ReviewPublisher,\n} from \"./github.ts\";\nimport type { ReviewPublicationPlan } from \"./render.ts\";\nimport type { ReviewHeadComparison, ReviewState } from \"./review-state.ts\";\nimport {\n MAX_CHANGED_FILES,\n MAX_FILE_CHARS,\n normalizeRepoRelativePath,\n PullRequestMetadata,\n PullRequestSource,\n ReviewInputViolation,\n} from \"./source.ts\";\n\n// ---------------------------------------------------------------------------\n// Deterministic in-memory adapters for both ports: a fixture pull request\n// serving the PullRequestSource, and a collecting ReviewPublisher recording\n// every plan. Tests, dry runs, and live smokes run against these with no\n// network and no credentials.\n// ---------------------------------------------------------------------------\n\n/** One fixture file: its changeset entry plus optional head content. */\nexport class FixtureFile extends Schema.Class<FixtureFile>(\"@effect-agent/pr-review/FixtureFile\")({\n file: ChangedFile,\n baseContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),\n headContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),\n}) {}\n\n/** A complete in-memory pull request for tests, dry runs, and live smokes. */\nexport class FixturePullRequest extends Schema.Class<FixturePullRequest>(\n \"@effect-agent/pr-review/FixturePullRequest\",\n)({\n metadata: PullRequestMetadata,\n files: Schema.Array(FixtureFile).check(Schema.isMaxLength(MAX_CHANGED_FILES)),\n}) {}\n\nconst requireChanged = (\n fixture: FixturePullRequest,\n path: string,\n): Effect.Effect<FixtureFile, ReviewInputViolation> => {\n const entry = fixture.files.find((candidate) => candidate.file.path === path);\n return entry === undefined\n ? Effect.fail(\n ReviewInputViolation.make({\n input: path,\n reason: \"Path is not part of this pull request's changeset.\",\n }),\n )\n : Effect.succeed(entry);\n};\n\n/** Deterministic `PullRequestSource` over one fixture pull request. */\nexport const fixturePullRequestSourceLayer = (\n fixture: FixturePullRequest,\n): Layer.Layer<PullRequestSource> => {\n const files = fixture.files.map((entry) =>\n entry.file.patch !== undefined\n ? entry.file\n : ChangedFile.make({\n ...entry.file,\n ...(entry.baseContent === undefined ? {} : { reviewBaseContent: entry.baseContent }),\n ...(entry.headContent === undefined ? {} : { reviewHeadContent: entry.headContent }),\n }),\n );\n return Layer.succeed(PullRequestSource)(\n PullRequestSource.of({\n metadata: Effect.succeed(fixture.metadata),\n changedFiles: Effect.succeed(files),\n anchorFiles: Effect.succeed(files),\n readFile: (path) =>\n Effect.gen(function* () {\n const relative = yield* normalizeRepoRelativePath(path);\n const entry = yield* requireChanged(fixture, relative);\n if (entry.headContent === undefined) {\n return yield* ReviewInputViolation.make({\n input: relative,\n reason: \"No head content is available for this file.\",\n });\n }\n return entry.headContent;\n }),\n }),\n );\n};\n\n/** In-memory publisher: records every plan and mints a deterministic receipt. */\nexport const collectingReviewPublisherLayer = (\n published: Ref.Ref<ReadonlyArray<ReviewPublicationPlan>>,\n): Layer.Layer<ReviewPublisher> =>\n Layer.succeed(ReviewPublisher)(\n ReviewPublisher.of({\n publish: (plan) =>\n Ref.update(published, (plans) => [...plans, plan]).pipe(\n Effect.flatMap(() => Ref.get(published)),\n Effect.map((plans) =>\n PublishedReview.make({\n reviewId: plans.length,\n url: `memory://review/${plans.length}`,\n event: plan.event,\n inlineComments: plan.comments.length,\n authorNodeId: \"BOT_memory-reviewer\",\n submittedAt: DateTime.makeUnsafe(\n `2026-01-01T00:00:${String(plans.length).padStart(2, \"0\")}Z`,\n ),\n }),\n ),\n ),\n }),\n );\n\n/** Static `PriorReviews` service for tests: fixed history and comparisons. */\nexport const staticPriorReviews = (\n fingerprint: Option.Option<string>,\n options: {\n readonly state?: Option.Option<ReviewState> | undefined;\n readonly comparison?: ReviewHeadComparison | undefined;\n } = {},\n): PriorReviews[\"Service\"] =>\n PriorReviews.of({\n latestFingerprint: Effect.succeed(fingerprint),\n latestState: Effect.succeed(options.state ?? Option.none()),\n compareHeads: () =>\n options.comparison === undefined\n ? Effect.fail(PriorReviewLookupFailure.make({ reason: \"no fixture comparison\" }))\n : Effect.succeed(options.comparison),\n });\n\n/** Layer form for consumers whose Effect explicitly requires `PriorReviews`. */\nexport const staticPriorReviewsLayer = (\n fingerprint: Option.Option<string>,\n options: {\n readonly state?: Option.Option<ReviewState> | undefined;\n readonly comparison?: ReviewHeadComparison | undefined;\n } = {},\n): Layer.Layer<PriorReviews> =>\n Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));\n"],"mappings":"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"}
1
+ {"version":3,"file":"testing.mjs","names":[],"sources":["../src/internal/scripted.ts","../src/internal/fan-out-scripted.ts","../src/internal/fixtures.ts"],"sourcesContent":["import { Effect, Layer, Ref, Schema, Stream } from \"effect\";\nimport { LanguageModel, Model, type Response } from \"effect/unstable/ai\";\n\nimport { CodeReview } from \"./review-agent.ts\";\n\n// ---------------------------------------------------------------------------\n// Deterministic offline model for the flat reviewer: a prompt-aware scripted\n// model that walks the real tool surface — list, diff, read — then returns\n// the scripted review as its terminal JSON. Decisions key on committed\n// history in the prompt, never on call order, so replays stay honest.\n// ---------------------------------------------------------------------------\n\nexport const OFFLINE_LIST_CALL_ID = \"list-1\";\nexport const OFFLINE_DIFF_CALL_ID = \"diff-1\";\nexport const OFFLINE_READ_CALL_ID = \"read-1\";\n\n/** Usage attached to EVERY scripted model turn; tests pin exact aggregates. */\nexport const SCRIPTED_TURN_USAGE = { inputTokens: 64, outputTokens: 48 } as const;\n\nconst scriptedUsage = {\n inputTokens: { total: SCRIPTED_TURN_USAGE.inputTokens },\n outputTokens: { total: SCRIPTED_TURN_USAGE.outputTokens },\n};\n\nexport const scriptedToolTurn = (\n ...calls: ReadonlyArray<Response.StreamPartEncoded>\n): ReadonlyArray<Response.StreamPartEncoded> => [\n ...calls,\n { type: \"finish\", reason: \"tool-calls\", usage: scriptedUsage },\n];\n\nexport const scriptedFinalParts = (text: string): ReadonlyArray<Response.StreamPartEncoded> => [\n { type: \"text-start\", id: \"code-review\" },\n { type: \"text-delta\", id: \"code-review\", delta: text },\n { type: \"text-end\", id: \"code-review\" },\n { type: \"finish\", reason: \"stop\", usage: scriptedUsage },\n];\n\n/** A prompt-keyed scripted LanguageModel with call and prompt observability. */\nexport const makePromptKeyedModel = (\n name: string,\n decide: (promptJson: string) => ReadonlyArray<Response.StreamPartEncoded>,\n) =>\n Effect.gen(function* () {\n const calls = yield* Ref.make(0);\n const prompts = yield* Ref.make<ReadonlyArray<string>>([]);\n const model = Model.make(\n \"scripted\",\n name,\n Layer.effect(\n LanguageModel.LanguageModel,\n LanguageModel.make({\n generateText: () => Effect.succeed([]),\n streamText: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* Ref.update(calls, (value) => value + 1);\n const promptJson = JSON.stringify(request.prompt);\n yield* Ref.update(prompts, (previous) => [...previous, promptJson]);\n return Stream.fromIterable(decide(promptJson));\n }),\n ),\n }),\n ),\n );\n return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };\n });\n\n/**\n * Build the offline scripted reviewer model. Turn 1 lists the changeset,\n * Turn 2 reads one file diff, Turn 3 reads head context, Turn 4 returns the\n * scripted review JSON.\n */\nexport const makeOfflineReviewerModel = (script: {\n readonly diffPath: string;\n readonly readPath: string;\n readonly review: CodeReview;\n}) =>\n makePromptKeyedModel(\"pr-review-offline\", (promptJson) => {\n if (promptJson.includes(OFFLINE_READ_CALL_ID)) {\n return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));\n }\n if (promptJson.includes(OFFLINE_DIFF_CALL_ID)) {\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_READ_CALL_ID,\n name: \"read_file\",\n params: { path: script.readPath },\n providerExecuted: false,\n });\n }\n if (promptJson.includes(OFFLINE_LIST_CALL_ID)) {\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_DIFF_CALL_ID,\n name: \"read_file_diff\",\n params: { path: script.diffPath },\n providerExecuted: false,\n });\n }\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_LIST_CALL_ID,\n name: \"list_changed_files\",\n params: { scope: \"all\" },\n providerExecuted: false,\n });\n });\n","import { Effect, Layer, Ref, Schema, Stream } from \"effect\";\nimport { LanguageModel, Model, type Response } from \"effect/unstable/ai\";\n\nimport { FileReviewReport, FileReviewRequest } from \"./fan-out.ts\";\nimport { CodeReview } from \"./review-agent.ts\";\nimport { makePromptKeyedModel, scriptedFinalParts, scriptedToolTurn } from \"./scripted.ts\";\n\n// Deterministic offline models for the complete fan-out protocol. Decisions\n// key on committed Tool Call IDs or work IDs in each prompt, never call order,\n// so bounded child concurrency cannot make the fixtures flaky.\n\nexport const OFFLINE_UNITS_CALL_ID = \"units-1\";\n\nexport const offlineUnitCallId = (workId: string): string => `delegate-${workId}`;\n\nexport type OfflineUnitCall = FileReviewRequest;\n\nconst scriptedUnitCallId = (calls: ReadonlyArray<OfflineUnitCall>, index: number): string => {\n const call = calls[index];\n if (call === undefined) return \"delegate-none\";\n const occurrence = calls\n .slice(0, index + 1)\n .filter((candidate) => candidate.workId === call.workId).length;\n const base = offlineUnitCallId(call.workId);\n return occurrence === 1 ? base : `${base}-${occurrence}`;\n};\n\nexport const makeOfflineFanOutCoordinatorModel = (script: {\n readonly discoveryCalls: ReadonlyArray<OfflineUnitCall>;\n readonly verificationCalls: ReadonlyArray<OfflineUnitCall>;\n readonly review: CodeReview;\n}) => {\n const firstDiscovery = scriptedUnitCallId(script.discoveryCalls, 0);\n const firstVerification = scriptedUnitCallId(script.verificationCalls, 0);\n return makePromptKeyedModel(\"pr-fanout-coordinator-offline\", (promptJson) => {\n if (script.discoveryCalls.length === 0 && promptJson.includes(OFFLINE_UNITS_CALL_ID)) {\n return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));\n }\n if (\n script.verificationCalls.length === 0\n ? promptJson.includes(firstDiscovery)\n : promptJson.includes(firstVerification)\n ) {\n return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));\n }\n if (promptJson.includes(firstDiscovery)) {\n return scriptedToolTurn(\n ...script.verificationCalls.map(\n (call, index): Response.StreamPartEncoded => ({\n type: \"tool-call\",\n id: scriptedUnitCallId(script.verificationCalls, index),\n name: \"delegate_file_review\",\n params: Schema.encodeSync(FileReviewRequest)(call),\n providerExecuted: false,\n }),\n ),\n );\n }\n if (promptJson.includes(OFFLINE_UNITS_CALL_ID)) {\n return scriptedToolTurn(\n ...script.discoveryCalls.map(\n (call, index): Response.StreamPartEncoded => ({\n type: \"tool-call\",\n id: scriptedUnitCallId(script.discoveryCalls, index),\n name: \"delegate_file_review\",\n params: Schema.encodeSync(FileReviewRequest)(call),\n providerExecuted: false,\n }),\n ),\n );\n }\n return scriptedToolTurn({\n type: \"tool-call\",\n id: OFFLINE_UNITS_CALL_ID,\n name: \"list_review_units\",\n params: { scope: \"all\" },\n providerExecuted: false,\n });\n });\n};\n\nexport type OfflineUnitOutcome =\n | { readonly _tag: \"report\"; readonly report: FileReviewReport }\n | { readonly _tag: \"malformed-output\" };\n\nexport interface OfflineUnitScript {\n readonly workId: string;\n readonly outcome: OfflineUnitOutcome;\n}\n\nexport const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitScript>) =>\n Effect.gen(function* () {\n const calls = yield* Ref.make(0);\n const prompts = yield* Ref.make<ReadonlyArray<string>>([]);\n const model = Model.make(\n \"scripted\",\n \"pr-fanout-file-reviewer-offline\",\n Layer.effect(\n LanguageModel.LanguageModel,\n LanguageModel.make({\n generateText: () => Effect.succeed([]),\n streamText: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* Ref.update(calls, (value) => value + 1);\n const promptJson = JSON.stringify(request.prompt);\n yield* Ref.update(prompts, (previous) => [...previous, promptJson]);\n const script = scripts.filter((candidate) =>\n promptJson.includes(`for ${candidate.workId} in host-planned unit`),\n );\n if (script.length !== 1) {\n return yield* Effect.die(\n new Error(\"The child prompt must name exactly one scripted work ID\"),\n );\n }\n const [selected] = script;\n if (selected === undefined) return yield* Effect.die(\"unreachable scripted match\");\n return Stream.fromIterable(\n scriptedFinalParts(\n selected.outcome._tag === \"report\"\n ? JSON.stringify(Schema.encodeSync(FileReviewReport)(selected.outcome.report))\n : \"this is not valid review JSON\",\n ),\n );\n }),\n ),\n }),\n ),\n );\n return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };\n });\n","import { DateTime, Effect, Layer, Option, Ref, Schema } from \"effect\";\n\nimport { ChangedFile } from \"./diff.ts\";\nimport {\n PriorReviewLookupFailure,\n PriorReviews,\n PublishedReview,\n ReviewPublisher,\n} from \"./github.ts\";\nimport type { ReviewPublicationPlan } from \"./render.ts\";\nimport type { ReviewHeadComparison, ReviewState } from \"./review-state.ts\";\nimport {\n MAX_CHANGED_FILES,\n MAX_FILE_CHARS,\n normalizeRepoRelativePath,\n PullRequestMetadata,\n PullRequestSource,\n ReviewInputViolation,\n} from \"./source.ts\";\n\n// ---------------------------------------------------------------------------\n// Deterministic in-memory adapters for both ports: a fixture pull request\n// serving the PullRequestSource, and a collecting ReviewPublisher recording\n// every plan. Tests, dry runs, and live smokes run against these with no\n// network and no credentials.\n// ---------------------------------------------------------------------------\n\n/** One fixture file: its changeset entry plus optional head content. */\nexport class FixtureFile extends Schema.Class<FixtureFile>(\"@effect-agent/pr-review/FixtureFile\")({\n file: ChangedFile,\n baseContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),\n headContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),\n}) {}\n\n/** A complete in-memory pull request for tests, dry runs, and live smokes. */\nexport class FixturePullRequest extends Schema.Class<FixturePullRequest>(\n \"@effect-agent/pr-review/FixturePullRequest\",\n)({\n metadata: PullRequestMetadata,\n files: Schema.Array(FixtureFile).check(Schema.isMaxLength(MAX_CHANGED_FILES)),\n}) {}\n\nconst requireChanged = (\n fixture: FixturePullRequest,\n path: string,\n): Effect.Effect<FixtureFile, ReviewInputViolation> => {\n const entry = fixture.files.find((candidate) => candidate.file.path === path);\n return entry === undefined\n ? Effect.fail(\n ReviewInputViolation.make({\n input: path,\n reason: \"Path is not part of this pull request's changeset.\",\n }),\n )\n : Effect.succeed(entry);\n};\n\n/** Deterministic `PullRequestSource` over one fixture pull request. */\nexport const fixturePullRequestSourceLayer = (\n fixture: FixturePullRequest,\n): Layer.Layer<PullRequestSource> => {\n const files = fixture.files.map((entry) =>\n entry.file.patch !== undefined\n ? entry.file\n : ChangedFile.make({\n ...entry.file,\n ...(entry.baseContent === undefined ? {} : { reviewBaseContent: entry.baseContent }),\n ...(entry.headContent === undefined ? {} : { reviewHeadContent: entry.headContent }),\n }),\n );\n return Layer.succeed(PullRequestSource)(\n PullRequestSource.of({\n metadata: Effect.succeed(fixture.metadata),\n changedFiles: Effect.succeed(files),\n anchorFiles: Effect.succeed(files),\n readFile: (path) =>\n Effect.gen(function* () {\n const relative = yield* normalizeRepoRelativePath(path);\n const entry = yield* requireChanged(fixture, relative);\n if (entry.headContent === undefined) {\n return yield* ReviewInputViolation.make({\n input: relative,\n reason: \"No head content is available for this file.\",\n });\n }\n return entry.headContent;\n }),\n }),\n );\n};\n\n/** In-memory publisher: records every plan and mints a deterministic receipt. */\nexport const collectingReviewPublisherLayer = (\n published: Ref.Ref<ReadonlyArray<ReviewPublicationPlan>>,\n): Layer.Layer<ReviewPublisher> =>\n Layer.succeed(ReviewPublisher)(\n ReviewPublisher.of({\n publish: (plan) =>\n Ref.update(published, (plans) => [...plans, plan]).pipe(\n Effect.flatMap(() => Ref.get(published)),\n Effect.map((plans) =>\n PublishedReview.make({\n reviewId: plans.length,\n url: `memory://review/${plans.length}`,\n event: plan.event,\n inlineComments: plan.comments.length,\n authorNodeId: \"BOT_memory-reviewer\",\n submittedAt: DateTime.makeUnsafe(\n `2026-01-01T00:00:${String(plans.length).padStart(2, \"0\")}Z`,\n ),\n }),\n ),\n ),\n }),\n );\n\n/** Static `PriorReviews` service for tests: fixed history and comparisons. */\nexport const staticPriorReviews = (\n fingerprint: Option.Option<string>,\n options: {\n readonly state?: Option.Option<ReviewState> | undefined;\n readonly comparison?: ReviewHeadComparison | undefined;\n } = {},\n): PriorReviews[\"Service\"] =>\n PriorReviews.of({\n latestFingerprint: Effect.succeed(fingerprint),\n latestState: Effect.succeed(options.state ?? Option.none()),\n compareHeads: () =>\n options.comparison === undefined\n ? Effect.fail(PriorReviewLookupFailure.make({ reason: \"no fixture comparison\" }))\n : Effect.succeed(options.comparison),\n });\n\n/** Layer form for consumers whose Effect explicitly requires `PriorReviews`. */\nexport const staticPriorReviewsLayer = (\n fingerprint: Option.Option<string>,\n options: {\n readonly state?: Option.Option<ReviewState> | undefined;\n readonly comparison?: ReviewHeadComparison | undefined;\n } = {},\n): Layer.Layer<PriorReviews> =>\n Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/pr-review",
3
- "version": "0.1.0-beta.20",
3
+ "version": "0.1.0-beta.22",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -24,7 +24,7 @@
24
24
  "@effect/ai-openai": "4.0.0-rc.110",
25
25
  "@effect/platform-node": "4.0.0-rc.110",
26
26
  "effect": "4.0.0-rc.110",
27
- "effect-agent": "0.1.0-beta.20"
27
+ "effect-agent": "0.1.0-beta.22"
28
28
  },
29
29
  "description": "Bounded, fail-closed GitHub pull-request reviewer built on the effect-agent public surface: schema-first review contracts, read-only tools, GitHub adapters, a configuration factory, and CLI + GitHub Actions entrypoints.",
30
30
  "license": "MIT",
package/src/action.ts CHANGED
@@ -3,6 +3,7 @@ import { Config, Console, Effect, FileSystem, Layer, Option, Redacted, Schema }
3
3
  import { BudgetExceeded, UsageBudgetLimits } from "effect-agent";
4
4
  import { FetchHttpClient } from "effect/unstable/http";
5
5
 
6
+ import type { ChangedFile } from "./internal/diff.ts";
6
7
  import { InvalidEffortInput, parseEffortPosition, type EffortPosition } from "./internal/effort.ts";
7
8
  import { PrReview, type RunReviewOptions } from "./internal/factory.ts";
8
9
  import { readGitHubEvent, resolveReviewTarget, gitHubReviewLayers } from "./internal/github-env.ts";
@@ -33,7 +34,11 @@ import {
33
34
  } from "./internal/review-state.ts";
34
35
  import { fanOutReviewBudgetLimits, reviewBudgetLimits } from "./internal/run.ts";
35
36
  import type { ReviewRunOutcome } from "./internal/run.ts";
36
- import { normalizeRepoRelativePath, PullRequestSource } from "./internal/source.ts";
37
+ import {
38
+ normalizeRepoRelativePath,
39
+ PullRequestSource,
40
+ type PullRequestMetadata,
41
+ } from "./internal/source.ts";
37
42
 
38
43
  // ---------------------------------------------------------------------------
39
44
  // The GitHub Actions entrypoint (deployment class E: one bounded ephemeral
@@ -122,7 +127,7 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
122
127
  const applyVerdict = yield* Config.boolean("PR_REVIEW_APPLY_VERDICT").pipe(
123
128
  Config.withDefault(false),
124
129
  );
125
- const fanOut = yield* Config.boolean("PR_REVIEW_FAN_OUT").pipe(Config.withDefault(false));
130
+ const fanOut = yield* Config.boolean("PR_REVIEW_FAN_OUT").pipe(Config.withDefault(true));
126
131
  const guidance = yield* Config.option(Config.nonEmptyString("PR_REVIEW_GUIDANCE"));
127
132
  const guidanceFile = yield* Config.option(Config.nonEmptyString("PR_REVIEW_GUIDANCE_FILE"));
128
133
  const ignoreRaw = yield* Config.string("PR_REVIEW_IGNORE").pipe(Config.withDefault(""));
@@ -262,6 +267,9 @@ const outcomeOutputs = (
262
267
  ["skipped", "false"],
263
268
  ["conclusion", conclusion],
264
269
  ["verdict", outcome.review.verdict],
270
+ ["input-coverage", outcome.inputCoverage.status],
271
+ ["review-assurance", outcome.assurance.status],
272
+ // Compatibility output: the precise outputs above own new integrations.
265
273
  ["coverage", outcome.coverage.status],
266
274
  ["review-mode", outcome.reviewMode ?? "full"],
267
275
  ["review-reason", outcome.reviewReason ?? "direct full review"],
@@ -285,7 +293,8 @@ const outcomeSummary = (
285
293
  "### Pull-request review",
286
294
  `- Check conclusion: **${conclusion}**`,
287
295
  `- Verdict: **${outcome.review.verdict}**`,
288
- `- Coverage: **${outcome.coverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
296
+ `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
297
+ `- 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}`,
289
298
  `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
290
299
  ...(modelLabel === undefined ? [] : [`- Model: \`${modelLabel}\``]),
291
300
  ...(outcome.usage === undefined
@@ -327,27 +336,31 @@ const resolveRunUrl = Effect.fn("resolveRunUrl")(function* () {
327
336
  return `${server}/${repository}/actions/runs/${runId}`;
328
337
  });
329
338
 
330
- /** The reviewer surface the action harness drives. `PrReview.make` and
331
- * `PrReview.makeFanOut` provide the state-selection effects; the legacy
332
- * fingerprint field remains for source compatibility with custom harnesses. */
339
+ /**
340
+ * The reviewer surface the action harness drives. `PrReview.make` and
341
+ * `PrReview.makeFanOut` provide the state-selection effects. A fingerprint is
342
+ * skip authority only when a profile fingerprint and authenticated review
343
+ * state bind it to settled assurance; a fingerprint-only custom harness
344
+ * remains source-compatible but deliberately runs instead of claiming green
345
+ * assurance from unauthenticated prior-review text.
346
+ */
333
347
  export interface HarnessedReviewer<E, R, FingerprintE, FingerprintR> {
334
348
  readonly run: (runOptions?: RunReviewOptions) => Effect.Effect<ReviewRunOutcome, E, R>;
349
+ /** Current effective changeset fingerprint; not standalone skip authority. */
335
350
  readonly fingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
336
351
  readonly profileFingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
337
352
  readonly snapshot?:
338
353
  | Effect.Effect<
339
354
  {
340
- readonly metadata: import("./internal/source.ts").PullRequestMetadata;
341
- readonly files: ReadonlyArray<import("./internal/diff.ts").ChangedFile>;
355
+ readonly metadata: PullRequestMetadata;
356
+ readonly files: ReadonlyArray<ChangedFile>;
342
357
  },
343
358
  FingerprintE,
344
359
  FingerprintR
345
360
  >
346
361
  | undefined;
347
362
  readonly filterFiles?:
348
- | ((
349
- files: ReadonlyArray<import("./internal/diff.ts").ChangedFile>,
350
- ) => ReadonlyArray<import("./internal/diff.ts").ChangedFile>)
363
+ | ((files: ReadonlyArray<ChangedFile>) => ReadonlyArray<ChangedFile>)
351
364
  | undefined;
352
365
  }
353
366
 
@@ -370,8 +383,17 @@ export const concludeReviewOutcome = (
370
383
  readonly conclusion: ReviewCheckConclusion;
371
384
  readonly reasons: ReadonlyArray<string>;
372
385
  } => {
373
- if (outcome.coverage.status === "incomplete") {
374
- return { conclusion: "incomplete", reasons: outcome.coverage.reasons };
386
+ if (outcome.inputCoverage.status === "incomplete") {
387
+ return { conclusion: "incomplete", reasons: outcome.inputCoverage.reasons };
388
+ }
389
+ if (outcome.assurance.status !== "settled") {
390
+ return {
391
+ conclusion: "incomplete",
392
+ reasons:
393
+ outcome.assurance.reasons.length > 0
394
+ ? outcome.assurance.reasons
395
+ : ["configured discovery and verification work did not settle"],
396
+ };
375
397
  }
376
398
  const reasons = blockingReasons({
377
399
  findings: outcome.activeFindings,
@@ -408,6 +430,8 @@ const skipCoveredReview = Effect.fn("skipCoveredReview")(function* (input: {
408
430
  ["skip-reason", input.reason],
409
431
  ...(input.fingerprint === undefined ? [] : ([["fingerprint", input.fingerprint]] as const)),
410
432
  ["conclusion", result.conclusion],
433
+ ["input-coverage", "complete"],
434
+ ["review-assurance", "settled"],
411
435
  ["coverage", "complete"],
412
436
  ["review-mode", "incremental"],
413
437
  ]);
@@ -520,8 +544,8 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
520
544
  pullRequestNumber: target.number,
521
545
  reason:
522
546
  equivalentPatchState.reviewedHeadSha === metadata.headSha
523
- ? "the current head already has complete stored review coverage"
524
- : "the effective pull-request patch is unchanged since the last complete review",
547
+ ? "the current head already has settled stored review assurance"
548
+ : "the effective pull-request patch is unchanged since the last settled review",
525
549
  state: equivalentPatchState,
526
550
  fingerprint: currentFingerprint,
527
551
  });
@@ -583,7 +607,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
583
607
  selection.files.length === 0 &&
584
608
  selection.priorState !== undefined
585
609
  ) {
586
- const reason = "no changed review scope since the last successfully reviewed head";
610
+ const reason = "no changed review scope since the last settled review head";
587
611
  return yield* skipCoveredReview({
588
612
  repository: target.repository,
589
613
  pullRequestNumber: target.number,
@@ -591,29 +615,6 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
591
615
  state: selection.priorState,
592
616
  });
593
617
  }
594
- } else if (options.skipUnchanged !== false && reviewer.fingerprint !== undefined) {
595
- // Preserve the pre-state custom harness contract explicitly. The
596
- // packaged reviewer always takes the authenticated state path above.
597
- const current = yield* reviewer.fingerprint;
598
- const history = options.priorReviews ?? (yield* PriorReviews);
599
- const latest = yield* history.latestFingerprint.pipe(
600
- Effect.orElseSucceed(() => Option.none<string>()),
601
- );
602
- if (Option.isSome(latest) && latest.value === current) {
603
- const reason = "changeset unchanged since the last review";
604
- yield* Console.log(
605
- `Skipping review of ${target.repository}#${target.number}: ${reason}.`,
606
- );
607
- yield* writeActionOutputs([
608
- ["skipped", "true"],
609
- ["skip-reason", reason],
610
- ["fingerprint", current],
611
- ["conclusion", "success"],
612
- ["coverage", "complete"],
613
- ]);
614
- yield* writeStepSummary(["### Pull-request review skipped", `- Reason: ${reason}`]);
615
- return { _tag: "Skipped", reason } satisfies ReviewActionResult;
616
- }
617
618
  }
618
619
  yield* Console.log(
619
620
  `Reviewing ${target.repository}#${target.number} (${options.post === false ? "dry run" : "posting"})...`,
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // The @effect-agent/pr-review public surface. Platform-free: host entrypoints
2
2
  // live behind the "./action" and "./cli" subpaths, deterministic test helpers
3
3
  // behind "./testing".
4
+ export * from "./internal/anchors.ts";
4
5
  export * from "./internal/diff.ts";
5
6
  export * from "./internal/coverage.ts";
6
7
  export * from "./internal/effort.ts";
@@ -0,0 +1,20 @@
1
+ import { commentableLines } from "./diff.ts";
2
+ import type { ChangedFile } from "./diff.ts";
3
+ import type { ReviewFinding } from "./review-agent.ts";
4
+
5
+ /** Why a finding cannot anchor to the current new-version diff, if any. */
6
+ export const anchorViolation = (
7
+ finding: ReviewFinding,
8
+ files: ReadonlyArray<ChangedFile>,
9
+ ): string | undefined => {
10
+ const file = files.find((candidate) => candidate.path === finding.path);
11
+ if (file === undefined) return "path is not part of the changeset";
12
+ if (file.patch === undefined) return "file has no anchorable textual diff";
13
+ if (finding.endLine < finding.startLine) return "endLine precedes startLine";
14
+ if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
15
+ const anchors = commentableLines(file.patch);
16
+ for (let line = finding.startLine; line <= finding.endLine; line += 1) {
17
+ if (!anchors.has(line)) return `line ${line} is not part of the diff`;
18
+ }
19
+ return undefined;
20
+ };