@effect-agent/pr-review 0.1.0-beta.21 → 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.
@@ -111,9 +111,51 @@ export class CandidateAssessment extends Schema.Class<CandidateAssessment>(
111
111
  )({
112
112
  candidateId: ReviewCandidateId,
113
113
  disposition: Schema.Literals(["confirmed", "rejected"]),
114
+ /**
115
+ * Exact suggestion settlement: required when the candidate finding carries
116
+ * a suggestion, forbidden otherwise. Untrusted child output cannot publish
117
+ * a GitHub replacement block by prompt compliance alone — the host keeps a
118
+ * confirmed finding's suggestion only on an exact "committable" settlement.
119
+ */
120
+ suggestion: Schema.optionalKey(
121
+ Schema.Literals(["committable", "not-committable"]).annotate({
122
+ description:
123
+ 'Required exactly when the candidate finding carries a suggestion: "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else. Forbidden for candidates without a suggestion.',
124
+ }),
125
+ ),
114
126
  rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
115
127
  }) {}
116
128
 
129
+ /**
130
+ * Exact suggestion settlement shape: a carried suggestion must be settled and
131
+ * nothing else may be. Enforced identically by the live delegation projection
132
+ * and the independent host coverage fold.
133
+ */
134
+ export const assessmentSettlesSuggestionExactly = (
135
+ assessment: CandidateAssessment,
136
+ candidate: ReviewCandidate,
137
+ ): boolean =>
138
+ candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== undefined
139
+ ? assessment.suggestion !== undefined
140
+ : assessment.suggestion === undefined;
141
+
142
+ /**
143
+ * Fail-closed publication of a confirmed finding: only an exact "committable"
144
+ * settlement keeps the suggestion; anything else publishes the finding with
145
+ * the suggestion stripped so unverified text can never become a one-click
146
+ * GitHub replacement block.
147
+ */
148
+ export const confirmedFindingForPublication = (
149
+ assessment: CandidateAssessment,
150
+ candidate: FindingCandidate,
151
+ ): ReviewFinding => {
152
+ if (candidate.finding.suggestion === undefined || assessment.suggestion === "committable") {
153
+ return candidate.finding;
154
+ }
155
+ const { suggestion: _stripped, ...finding } = candidate.finding;
156
+ return ReviewFinding.make(finding);
157
+ };
158
+
117
159
  /**
118
160
  * Concern candidates need explicit paths internally to bind the claim to
119
161
  * scheduled evidence. The verifier receives the complete bounded unit so it
@@ -260,7 +302,8 @@ export const makeFileReviewerInstructions =
260
302
  "Independently verify every candidate in the input. You did not receive another reviewer's transcript or reasoning; use only the candidate claim and bounded evidence.",
261
303
  "The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
262
304
  "For each candidate, try to falsify it first. Confirm only when the cited behavior is supported and actionable. Reject unsupported, speculative, duplicate, or non-actionable candidates.",
263
- 'Return ONLY JSON with phase "verification", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {"candidateId": <exact id>, "disposition": <"confirmed" | "rejected">, "rationale": <bounded evidence-based reason>}. Never add or omit an id.',
305
+ 'Return ONLY JSON with phase "verification", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {"candidateId": <exact id>, "disposition": <"confirmed" | "rejected">, "suggestion": <"committable" | "not-committable", present exactly when the candidate finding carries a suggestion>, "rationale": <bounded evidence-based reason>}. Never add or omit an id.',
306
+ 'Settle every carried suggestion independently of the claim: answer "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else — it compiles in context and preserves the finding\'s intent, never prose describing a change. Otherwise answer "not-committable"; the host then publishes the confirmed finding without its suggestion. Omit the assessment "suggestion" field for candidates without one.',
264
307
  ].join("\n");
265
308
  }
266
309
  const focus =
@@ -275,6 +318,8 @@ export const makeFileReviewerInstructions =
275
318
  "The discovery evidence array contains every complete shard in the unit. Review every entry and every shard of a multi-shard path. A later independent verifier, not you, decides which candidates publish.",
276
319
  "When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
277
320
  `Return ONLY JSON with phase "discovery", the exact workId/unitId, up to ${MAX_CHILD_FINDINGS} findings, up to ${MAX_CHILD_CONCERNS} concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= ${MAX_WALKTHROUGH_SUMMARY_CHARS} chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects.`,
321
+ 'Each finding is {"path": <a unit file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL problem-kind label>, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement source code for exactly lines startLine..endLine, ready to commit>}.',
322
+ 'Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement source for every line in the range and nothing else — never prose describing the change, which belongs in "body".',
278
323
  ].join("\n");
279
324
  };
280
325
 
@@ -462,17 +507,28 @@ const projectReviewResult = (
462
507
  rejectWork(report.workId, "verification output contained discovery-only fields"),
463
508
  );
464
509
  }
465
- const expectedIds = new Set(request.candidates.map((candidate) => candidate.candidateId));
510
+ const expectedById = new Map(
511
+ request.candidates.map((candidate) => [candidate.candidateId, candidate] as const),
512
+ );
466
513
  const assessedIds = new Set<string>();
467
514
  for (const assessment of report.assessments) {
468
- if (!expectedIds.has(assessment.candidateId) || assessedIds.has(assessment.candidateId)) {
515
+ const candidate = expectedById.get(assessment.candidateId);
516
+ if (candidate === undefined || assessedIds.has(assessment.candidateId)) {
469
517
  return Effect.fail(
470
518
  rejectWork(report.workId, "verification output did not assess the exact candidate set"),
471
519
  );
472
520
  }
521
+ if (!assessmentSettlesSuggestionExactly(assessment, candidate)) {
522
+ return Effect.fail(
523
+ rejectWork(
524
+ report.workId,
525
+ "verification output did not settle suggestion publication exactly",
526
+ ),
527
+ );
528
+ }
473
529
  assessedIds.add(assessment.candidateId);
474
530
  }
475
- if (assessedIds.size !== expectedIds.size) {
531
+ if (assessedIds.size !== expectedById.size) {
476
532
  return Effect.fail(
477
533
  rejectWork(report.workId, "verification output did not assess the exact candidate set"),
478
534
  );
@@ -346,7 +346,12 @@ export class ReviewFinding extends Schema.Class<ReviewFinding>(
346
346
  title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
347
347
  body: Schema.NonEmptyString.check(Schema.isMaxLength(2_000)),
348
348
  /** Replacement for exactly lines startLine..endLine; omit when unsure. */
349
- suggestion: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_000))),
349
+ suggestion: Schema.optionalKey(
350
+ Schema.String.annotate({
351
+ description:
352
+ "Committable replacement source code for exactly lines startLine..endLine: the full replacement for every line in the range and nothing else — never prose describing the change, which belongs in body.",
353
+ }).check(Schema.isMaxLength(2_000)),
354
+ ),
350
355
  }) {}
351
356
 
352
357
  export const ReviewVerdict = Schema.Literals(["approve", "comment", "request-changes"]);