1717 * artifact, so a prompt-injected `Read` of a secret-bearing path can't
1818 * smuggle a credential out through the artifact even though the posted
1919 * review is already scrubbed at render time.
20- * - `post` — posts the consolidated review, THEN best-effort supersedes any
21- * prior AI review on the PR (the marker/dedup guard in `resolve.ts` should
22- * normally prevent a second run, but `/ai-review` lets a maintainer force
23- * one; posting before superseding, and treating the supersede as
24- * best-effort, means a cosmetic supersede failure can never cost the real
25- * review).
20+ * - `post` — snapshots the PR's prior AI reviews, posts the consolidated
21+ * review, THEN best-effort supersedes the snapshotted ones (the
22+ * marker/dedup guard in `resolve.ts` should normally prevent a second
23+ * run, but `/ai-review` lets a maintainer force one). The snapshot must
24+ * happen BEFORE the POST — the fresh review is itself a marker-bearing
25+ * bot review, so a post-hoc listing would sweep it into its own
26+ * supersede pass and every new review would collapse itself. Posting
27+ * before superseding, and treating both the snapshot and the supersede
28+ * as best-effort, means a cosmetic failure can never cost the real
29+ * review.
2630 *
2731 * `parseDiffAnchors`, `partitionFindings`, `renderReviewBody`,
2832 * `renderInlineComment`, `buildReviewPayload`, `foldInlineCommentsIntoBody`,
3943export const AI_REVIEW_MARKER = "<!-- supabase-ai-review -->" ;
4044const SUPERSEDED_SUMMARY = "Superseded by a newer AI review" ;
4145/** Hidden marker `isSuperseded` looks for. Kept out of the human-readable
42- * `SUPERSEDED_SUMMARY` text and stripped by `sanitizeModelText` so a model
46+ * `SUPERSEDED_SUMMARY` text and broken by `sanitizeModelText` so a model
4347 * can't forge or evade a supersede by echoing the visible text into a
4448 * `claim`/`summary` field. */
4549const SUPERSEDED_MARKER = "<!-- supabase-ai-review:superseded -->" ;
@@ -502,7 +506,7 @@ export function computeVerdictCounts(findings: MergedFinding[]): VerdictCounts {
502506
503507const MENTION_PATTERN = / @ (? = \w ) / g;
504508const ISSUE_REF_PATTERN = / # (? = \d ) / g;
505- const HTML_COMMENT_PATTERN = / < ! - - [ \s \S ] * ? - - > / g;
509+ const HTML_COMMENT_OPENER_PATTERN = / < ! - - / g;
506510
507511const REDACTED_SECRET = "«redacted»" ;
508512
@@ -560,8 +564,8 @@ export function redactSecretsDeep(value: unknown): unknown {
560564
561565/**
562566 * Neutralizes a model-provided string before it's rendered into a
563- * `github-actions[bot]` review: redacts secret-shaped substrings first, strips
564- * HTML comments (so injected diff content can't forge the hidden
567+ * `github-actions[bot]` review: redacts secret-shaped substrings first, breaks
568+ * HTML comment openers (so injected diff content can't forge the hidden
565569 * `AI_REVIEW_MARKER`/`SUPERSEDED_MARKER` comments), then breaks
566570 * `@mention`/`#123` syntax with a zero-width HTML comment so GitHub never
567571 * renders them as a live mention or issue reference. Pure; apply to every
@@ -570,7 +574,7 @@ export function redactSecretsDeep(value: unknown): unknown {
570574 */
571575export function sanitizeModelText ( text : string ) : string {
572576 return redactSecrets ( text )
573- . replace ( HTML_COMMENT_PATTERN , "" )
577+ . replace ( HTML_COMMENT_OPENER_PATTERN , "<\u200B!-- " )
574578 . replace ( MENTION_PATTERN , "@<!---->" )
575579 . replace ( ISSUE_REF_PATTERN , "#<!---->" ) ;
576580}
@@ -842,42 +846,61 @@ export interface ReviewIo {
842846 ) => Promise < { status : number ; body ?: string } > ;
843847}
844848
845- /** Wraps every prior AI review/comment on the PR in a superseded `<details>` block. Idempotent. */
846- async function supersedePriorRuns ( io : ReviewIo , prNumber : number ) : Promise < void > {
847- const [ reviews , comments ] = await Promise . all ( [
848- io . listReviews ( prNumber ) ,
849- io . listIssueComments ( prNumber ) ,
850- ] ) ;
849+ /** The prior AI reviews/comments this run will supersede, snapshotted BEFORE
850+ * the new review is posted. */
851+ interface PriorRuns {
852+ reviews : MarkedEntry [ ] ;
853+ comments : MarkedEntry [ ] ;
854+ }
851855
852- for ( const review of reviews ) {
853- if (
854- review . authorLogin !== WORKFLOW_BOT_LOGIN ||
855- ! review . body . includes ( AI_REVIEW_MARKER ) ||
856- isSuperseded ( review . body )
857- ) {
858- continue ;
859- }
860- await io . updateReviewBody ( prNumber , review . id , supersededBody ( review . body ) ) ;
861- }
856+ /** A marker-bearing AI review/comment by the workflow bot that hasn't been
857+ * superseded yet — the only kind a supersede pass may wrap. */
858+ function isSupersedableAiEntry ( entry : MarkedEntry ) : boolean {
859+ return (
860+ entry . authorLogin === WORKFLOW_BOT_LOGIN &&
861+ entry . body . includes ( AI_REVIEW_MARKER ) &&
862+ ! isSuperseded ( entry . body )
863+ ) ;
864+ }
862865
863- for ( const comment of comments ) {
864- if (
865- comment . authorLogin !== WORKFLOW_BOT_LOGIN ||
866- ! comment . body . includes ( AI_REVIEW_MARKER ) ||
867- isSuperseded ( comment . body )
868- ) {
869- continue ;
870- }
871- await io . updateIssueCommentBody ( comment . id , supersededBody ( comment . body ) ) ;
866+ /** Snapshots the prior AI reviews/comments to supersede. MUST run before the
867+ * new review is posted: the fresh review is itself a marker-bearing bot
868+ * review, so a post-hoc listing would sweep it into its own supersede pass
869+ * and every new review would immediately collapse as "superseded".
870+ * Best-effort — a listing failure degrades to an empty snapshot (prior runs
871+ * stay unwrapped) rather than costing the real review. */
872+ async function listPriorRunsBestEffort ( io : ReviewIo , prNumber : number ) : Promise < PriorRuns > {
873+ try {
874+ const [ reviews , comments ] = await Promise . all ( [
875+ io . listReviews ( prNumber ) ,
876+ io . listIssueComments ( prNumber ) ,
877+ ] ) ;
878+ return {
879+ reviews : reviews . filter ( isSupersedableAiEntry ) ,
880+ comments : comments . filter ( isSupersedableAiEntry ) ,
881+ } ;
882+ } catch ( error ) {
883+ console . warn ( `Could not list prior AI review runs on PR #${ prNumber } : ${ String ( error ) } ` ) ;
884+ return { reviews : [ ] , comments : [ ] } ;
872885 }
873886}
874887
875- /** Best-effort wrapper around `supersedePriorRuns`: a cosmetic failure here
876- * (e.g. a transient 404 on a review that was deleted mid-run) must never
877- * fail the pipeline after the real review/notice has already been posted. */
878- async function supersedePriorRunsBestEffort ( io : ReviewIo , prNumber : number ) : Promise < void > {
888+ /** Wraps the snapshotted prior AI reviews/comments in a superseded `<details>`
889+ * block. Best-effort: a cosmetic failure here (e.g. a transient 404 on a
890+ * review that was deleted mid-run) must never fail the pipeline after the
891+ * real review has already been posted. */
892+ async function supersedePriorRunsBestEffort (
893+ io : ReviewIo ,
894+ prNumber : number ,
895+ prior : PriorRuns ,
896+ ) : Promise < void > {
879897 try {
880- await supersedePriorRuns ( io , prNumber ) ;
898+ for ( const review of prior . reviews ) {
899+ await io . updateReviewBody ( prNumber , review . id , supersededBody ( review . body ) ) ;
900+ }
901+ for ( const comment of prior . comments ) {
902+ await io . updateIssueCommentBody ( comment . id , supersededBody ( comment . body ) ) ;
903+ }
881904 } catch ( error ) {
882905 console . warn ( `Could not supersede prior AI review runs on PR #${ prNumber } : ${ String ( error ) } ` ) ;
883906 }
@@ -893,6 +916,10 @@ export async function postConsolidatedReview(
893916 const anchors = parseDiffAnchors ( diff ) ;
894917 const payload = buildReviewPayload ( review , anchors , footer ) ;
895918
919+ // Snapshot before the POST — see `listPriorRunsBestEffort` for why the
920+ // ordering is load-bearing.
921+ const prior = await listPriorRunsBestEffort ( io , prNumber ) ;
922+
896923 const result = await io . postReview ( prNumber , payload ) ;
897924 if ( result . status === 422 && payload . comments . length > 0 ) {
898925 console . warn (
@@ -915,7 +942,7 @@ export async function postConsolidatedReview(
915942 ) ;
916943 }
917944
918- await supersedePriorRunsBestEffort ( io , prNumber ) ;
945+ await supersedePriorRunsBestEffort ( io , prNumber , prior ) ;
919946}
920947
921948// --- Real GitHub I/O (only runs when executed directly) ---
0 commit comments