pi-pr-review 1.10.1 → 1.10.2

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.10.1"
2
+ ".": "1.10.2"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.2](https://github.com/10ego/pi-pr-review/compare/v1.10.1...v1.10.2) (2026-07-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **publish:** preserve findings with duplicate anchors ([#25](https://github.com/10ego/pi-pr-review/issues/25)) ([1df420f](https://github.com/10ego/pi-pr-review/commit/1df420f30b1dd1a74017c0442b2a880949d4b864))
9
+
3
10
  ## [1.10.1](https://github.com/10ego/pi-pr-review/compare/v1.10.0...v1.10.1) (2026-07-14)
4
11
 
5
12
 
package/README.md CHANGED
@@ -184,7 +184,7 @@ Publishing is off by default.
184
184
 
185
185
  The extension—not the model—owns publishing. It creates one formal review with the event hardcoded to `COMMENT`; it never submits `APPROVE` or `REQUEST_CHANGES`. Before writing, it verifies the current PR head, validates inline anchors, and checks for a review of the same head by the current GitHub identity.
186
186
 
187
- Closed or merged PRs use a body-only review. Open PRs attach eligible P0–P3 findings as inline comments and keep nits or off-diff findings in the review body.
187
+ Closed or merged PRs use a body-only review. Open PRs attach eligible P0–P3 findings as inline comments and keep nits or off-diff findings in the review body. When multiple findings target the same diff anchor, the first is attached inline and later findings remain in the review body so publication neither fails nor drops review content.
188
188
 
189
189
  After a review completes, you can directly ask the agent to “post the inline review” or “post it as an inline review.” The extension handles that request directly before an agent turn, selecting the latest cached review for an unnumbered request or the named PR in “publish the review for PR #123.” Only fresh interactive/RPC input can trigger this path; extension-generated, queued, or steering input cannot. The extension publishes only validated cached content, never replacement model-authored text, and never starts or reruns review agents. A direct request permits stale publication.
190
190
 
@@ -734,11 +734,20 @@ export function buildReviewSummary(review: ReviewLike, inlineComments: PublishCo
734
734
  lines.push("### Strengths", "", ...review.strengths.map((strength) => `- ${String(strength).replace(/^\s*-\s*/, "").trim()}`), "");
735
735
  }
736
736
  const findings = Array.isArray(review.findings) ? review.findings : [];
737
- const inlineAnchors = new Set(inlineComments.map(publishCommentAnchor));
737
+ const inlineAnchors = new Map<string, number>();
738
+ for (const comment of inlineComments) {
739
+ const anchor = publishCommentAnchor(comment);
740
+ inlineAnchors.set(anchor, (inlineAnchors.get(anchor) ?? 0) + 1);
741
+ }
738
742
  const summaryFindings = findings.filter((finding) => {
739
743
  if (!finding.code_location?.commentable || !isInlineSeverity(finding)) return true;
740
744
  const anchor = findingAnchor(finding);
741
- return !anchor || !inlineAnchors.has(anchor);
745
+ if (!anchor) return true;
746
+ const remaining = inlineAnchors.get(anchor) ?? 0;
747
+ if (remaining === 0) return true;
748
+ if (remaining === 1) inlineAnchors.delete(anchor);
749
+ else inlineAnchors.set(anchor, remaining - 1);
750
+ return false;
742
751
  });
743
752
  lines.push(
744
753
  `### Findings — ${findings.length} total (${inlineComments.length} inline, ${summaryFindings.length} summary-only)`,
@@ -879,11 +888,6 @@ export function validateInlineComments(
879
888
  continue;
880
889
  }
881
890
  const anchor = `${path}:${side}:${start}:${end}`;
882
- if (anchors.has(anchor)) {
883
- errors.push(`${label}: duplicate inline anchor`);
884
- continue;
885
- }
886
- anchors.add(anchor);
887
891
  const body = [`**${String(finding.title ?? "Review finding").trim()}**`, finding.body?.trim()]
888
892
  .filter(Boolean)
889
893
  .join("\n\n");
@@ -895,6 +899,9 @@ export function validateInlineComments(
895
899
  errors.push(`${label}: comment body contains a reserved pi-pr-review marker`);
896
900
  continue;
897
901
  }
902
+ // GitHub receives one comment per anchor; later findings remain in the top-level summary.
903
+ if (anchors.has(anchor)) continue;
904
+ anchors.add(anchor);
898
905
  comments.push({
899
906
  path,
900
907
  body,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pr-review",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and safe COMMENT-only publishing.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -194,7 +194,7 @@ The orchestrator must never call `gh` to post comments or reviews. Always finish
194
194
  - `--comment` → force publication for this run, but never bypass validation, stale-head checks, or duplicate checks
195
195
  - `--no-comment` → suppress publication for this run
196
196
 
197
- When enabled, the extension creates exactly one formal GitHub pull-request review. Its top-level body contains the overview, verification, strengths, suggested verdict, counts, nits, and findings that cannot be attached inline. Eligible P0–P3 diff-anchored findings are attached as inline comments within that same review and are omitted from the top-level issue list to avoid duplication. The API event is hardcoded to `COMMENT`: publication never sends `APPROVE` or `REQUEST_CHANGES`, even when the suggested verdict is `request_changes`. It appends the same-head marker, verifies the current head, validates every inline anchor against GitHub diff metadata, and refuses partial open-PR publication. For a known closed/merged PR it requires either trusted `--include-closed`/`--review-closed` invocation authority or the one-shot affirmative confirmation flow, then posts one body-only `COMMENT` review with each inline finding folded into the body exactly once. Unknown lifecycle states and unconfirmed non-open writes fail without posting or falling back to an issue comment.
197
+ When enabled, the extension creates exactly one formal GitHub pull-request review. Its top-level body contains the overview, verification, strengths, suggested verdict, counts, nits, and findings that cannot be attached inline. Eligible P0–P3 diff-anchored findings are attached as inline comments within that same review and are omitted from the top-level issue list to avoid duplication. If multiple findings share one diff anchor, the first is attached inline and later findings remain in the top-level body instead of failing publication or dropping content. The API event is hardcoded to `COMMENT`: publication never sends `APPROVE` or `REQUEST_CHANGES`, even when the suggested verdict is `request_changes`. It appends the same-head marker, verifies the current head, validates every inline anchor against GitHub diff metadata, and refuses partial open-PR publication. For a known closed/merged PR it requires either trusted `--include-closed`/`--review-closed` invocation authority or the one-shot affirmative confirmation flow, then posts one body-only `COMMENT` review with each inline finding folded into the body exactly once. Unknown lifecycle states and unconfirmed non-open writes fail without posting or falling back to an issue comment.
198
198
 
199
199
  The extension caches the latest valid completed review per repository and PR before publication preflight in the current Pi session. The session-backed cache survives extension reloads and session resumes but is bound to the originating session instance and repository. On a later turn, the extension intercepts that direct input before an agent turn, publishes only extension-cached content, permits stale publication, and cannot rerun review agents. Stale publication is also enabled by default through the invocation-captured `allowStalePublish` setting. Every stale review is body-only, displays a warning with the reviewed and preflight-observed commit hashes, and omits potentially invalid inline anchors. If the captured setting disabled stale publication, the user can still run `/pr-review-publish <PR-NUM> --allow-stale`. Never rerun the review merely to change posting intent, and never attempt a direct GitHub write yourself.
200
200
 
@@ -648,6 +648,34 @@ describe("atomic COMMENT review payload", () => {
648
648
  expect(summary).toContain("[P3] Summary-only collision");
649
649
  });
650
650
 
651
+ test("summarizes additional inline findings that share an anchor", () => {
652
+ const colliding: ReviewLike = JSON.parse(JSON.stringify(review));
653
+ colliding.findings!.push({
654
+ title: "[P2] Preserve the second issue",
655
+ severity: "P2",
656
+ blocking: false,
657
+ body: "This distinct issue targets the same diff range.",
658
+ confidence_score: 0.85,
659
+ code_location: {
660
+ absolute_file_path: "src/parser.ts",
661
+ line_range: { start: 2, end: 3 },
662
+ side: "RIGHT",
663
+ commentable: true,
664
+ },
665
+ });
666
+
667
+ const validated = validateInlineComments(colliding, changedFiles);
668
+ expect(validated.errors).toEqual([]);
669
+ expect(validated.comments).toHaveLength(1);
670
+ expect(validated.comments[0]?.body).toContain("[P2] Handle empty input");
671
+
672
+ const summary = buildReviewSummary(colliding, validated.comments);
673
+ expect(summary).toContain("3 total (1 inline, 2 summary-only)");
674
+ expect(summary).not.toContain("#### [P2] Handle empty input");
675
+ expect(summary).toContain("#### [P2] Preserve the second issue");
676
+ expect(summary).toContain("This distinct issue targets the same diff range.");
677
+ });
678
+
651
679
  test("rejects anchors outside changed diff metadata", () => {
652
680
  const invalid: ReviewLike = JSON.parse(JSON.stringify(review));
653
681
  invalid.findings![0]!.code_location!.line_range = { start: 20, end: 20 };