support

package
v0.0.0-...-ae7e76d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const MentionToken = "@code-guru"

MentionToken is the literal the bot looks for in a user's PR comment to treat the comment as a re-review request. Case-insensitive match is performed by HasMention; word-boundary checks prevent a substring match against unrelated `@code-guru-foo` mentions.

Variables

View Source
var ErrUnparseableResponse = errors.New("ai response is not valid JSON, even after repair")

ErrUnparseableResponse is returned when the AI response cannot be parsed even after a repair pass. Callers in the command layer treat this as a hard failure so no malformed JSON ends up posted to a PR thread.

Functions

func BuildReviewConversation

func BuildReviewConversation(
	comments []forgeEntities.PullRequestComment,
	isBot func(author string) bool,
	liveFiles map[string]struct{},
) []entities.ReviewThread

BuildReviewConversation assembles the bot's inline review threads on a PR into a chronologically-ordered list of ReviewThreads, each one rooted on a previous bot comment and carrying every reply (user follow-up, bot self-reply, anyone else) in dialogue order.

The walk is deliberately rooted on bot-authored top-level comments only: those are the threads the LLM should re-read on a re-review, because they are the conversations the bot started. User-only threads (reviewers asking each other questions, off-topic chat) are not relevant input to the AI and would dilute the prompt.

Identifying "the bot's" comments is provider-specific (GitHub uses `<app-name>[bot]`, Azure DevOps uses the bot's account uniqueName); rather than hard-coding a list, the caller passes a predicate so the matcher stays out of this package and the assembler stays pure.

`liveFiles` is the set of file paths in the current PR diff. Threads anchored to a file outside that set are dropped — without the filter, a re-review would see prior bot comments on lines that are no longer in the diff and try to "respond" inline on a stale anchor (the post-pipeline's `dropStaleComments` would then drop them, but only after the LLM has already wasted tokens deliberating). Passing nil keeps every thread regardless of file (used by tests that have no diff to compare against).

Algorithm:

  1. Collect every comment whose author satisfies isBot AND that is a top-level inline comment (Line > 0, InReplyToID == 0) AND that is anchored to a file present in liveFiles. Each becomes a thread root.
  2. For every other inline comment whose InReplyToID points at one of those roots (directly or transitively via a chain of replies), append it to that thread's Comments.
  3. Sort each thread's reply chain by comment ID (stable proxy for creation order — every provider returns IDs that monotonically increase by creation time).
  4. Sort the resulting threads by FilePath + Line so the prompt's conversation block reads top-to-bottom alongside the diff.

PR-wide comments (Line == 0) are skipped: they are the bot's markers / completion annotations / failure notices, not review findings the LLM should re-engage with.

func BuildSystemPrompt

func BuildSystemPrompt(rules []entities.Rule) string

BuildSystemPrompt is the first-pass shape: no thread_resolutions schema or rules. Kept as the default so first-pass reviews stay byte-for-byte identical to the pre-resolution prompt — the resolution-aware additions are scoped to the re-review path via BuildSystemPromptForReReview.

func BuildSystemPromptFor

func BuildSystemPromptFor(request entities.ReviewRequest) string

BuildSystemPromptFor dispatches to the appropriate system prompt builder based on whether the review request carries a conversation. All AI backends share this helper so the "first-pass stays the pre-resolution shape, re-review grows the resolution rules" invariant is enforced in exactly one place — without it, each backend would have to repeat the conditional and a future backend could silently regress to always emitting the resolution-aware prompt.

func BuildSystemPromptForReReview

func BuildSystemPromptForReReview(rules []entities.Rule) string

BuildSystemPromptForReReview adds the thread_resolutions schema + rules to the system prompt. Used only on the mention re-review path where a conversation exists for the LLM to classify. Splitting the two functions (rather than passing a flag) keeps the call sites self-documenting and stops the resolution rules from leaking into reviews that have no prior threads to act on.

func BuildUserPrompt

func BuildUserPrompt(title string, sourceBranch string, targetBranch string, diffs []entities.FileDiff) string

BuildUserPrompt assembles the user prompt from PR metadata and file diffs.

func BuildUserPromptWithConversation

func BuildUserPromptWithConversation(
	title string,
	sourceBranch string,
	targetBranch string,
	diffs []entities.FileDiff,
	threads []entities.ReviewThread,
) string

BuildUserPromptWithConversation extends BuildUserPrompt with a "Prior review conversation" block rendered before the diff. Each thread shows the original bot comment plus every reply in chronological order so the LLM can read the dialogue (often the user pushing back, asking for clarification, or saying the comment was wrong) before deciding whether to repeat / withdraw / respond to the original finding.

When threads is empty the function produces the same output as BuildUserPrompt — no "Prior review conversation" header, no extra guidance lines. This keeps first-pass reviews byte-for-byte identical to the pre-conversation prompt and avoids drifting the LLM's output shape on the path where there is no conversation to read.

Each rendered thread carries a synthetic identifier `T<n>` (1-indexed over the threads slice) in the header — `### Thread T1 on <file>:<line>`. That id is what the resolution-aware re-review path relies on to disambiguate two prior bot threads anchored to the same file:line: without it, the post-pipeline's `applyThreadResolutions` would collapse both entries onto one map key and silently lose every resolution past the first. The id is rebuilt in the same order on the post side so the LLM and the bot agree on which thread `T1` refers to.

On a re-review the conversation block is followed by a short instruction telling the model to integrate the dialogue: address the user's points instead of repeating the same finding, withdraw when the user has correctly identified a false positive, and surface only NEW findings the diff actually warrants. The response schema is unchanged — the model still emits a `comments` array; the instruction tunes WHICH comments it emits, not HOW.

func ClassifyFile

func ClassifyFile(path string) string

ClassifyFile returns the rule category for a file path based on its extension. Returns an empty string if the extension is not recognized.

func ClassifyFiles

func ClassifyFiles(paths []string) []string

ClassifyFiles returns the unique set of rule categories for the given file paths.

func HasCompletedReviewMarker

func HasCompletedReviewMarker(bodies []string) bool

HasCompletedReviewMarker returns true when any of the supplied PR-wide comment bodies looks like the bot's "review complete" or "review failed" annotation. The check is intentionally substring- based (not exact-match) so a future tweak to the annotation body does not accidentally bypass the gate; the marker is rare enough in real human comments that the false-positive risk is acceptable.

Used by `ReviewCommand.Execute` as the review-once-per-PR gate: when this returns true and the user did NOT mention the bot, the review is short-circuited so the PR is not flooded with duplicate reviews on every push.

func HasMention

func HasMention(body string) bool

HasMention returns true when the comment body contains a `@code-guru` mention. Case-insensitive; rejects substrings that continue past the token (e.g. `@code-guru-bot` is NOT a match because the next byte is not whitespace / punctuation / EOF). A future refactor that needs to distinguish between "mention" and "mention-with-instructions" can extend this without touching the call sites.

func IsBotAuthor

func IsBotAuthor() func(string) bool

IsBotAuthor returns a predicate the conversation walker uses to identify the bot's own comments. The match is **strict** so user names that happen to contain `code-guru` as a substring (e.g. `code-guru-fan`, `alice+code-guru@example.com`) are NOT pulled into the conversation context as if they were prior bot findings.

The author matches when, after a case-insensitive comparison, the string starts with the literal `code-guru` AND the next character is one of:

  • end of string (the bot identity is exactly `code-guru`)
  • `[` — the GitHub App login shape (`code-guru[bot]`)
  • `@` — the Azure DevOps PAT-identity shape (`code-guru@<tenant>`)

Anything else is rejected: a continuation alphanumeric / `-` / `+` would mean the `code-guru` is part of a longer identifier (a real user with a coincidentally matching prefix), and a leading `+` or `.` (as in `alice+code-guru@…`) means `code-guru` is in the local part of someone else's email.

Returned as a closure (rather than a free function) so a future configuration can override the matcher per deployment without touching the assembler.

func LookupChunkByPath

func LookupChunkByPath(chunks map[string]string, path string) (string, bool)

LookupChunkByPath returns the chunk for the given path, normalising the caller's path to match the splitter's convention (no leading slash). The splitter keys chunks by the bare new-side path because the `diff --git a/X b/X` line never carries a leading slash, but Azure DevOps's `GetPullRequestFiles` returns paths like `/README.md` — a direct lookup would silently miss for every ADO PR. Centralising the normalisation here keeps both providers wired through the same path.

func MapVerdictToReview

func MapVerdictToReview(verdict, summary string) (forgeEntities.ReviewSubmission, bool)

MapVerdictToReview translates a code-guru verdict string and its summary body into a gitforge ReviewSubmission suitable for SubmitPullRequestReview.

Cross-vocabulary mapping (see verdict constants for why both vocabularies reach this helper):

approve         -> ReviewVerdictApprove
reject          -> ReviewVerdictRequestChanges  (trivial detector vocab)
request_changes -> ReviewVerdictRequestChanges  (LLM parser vocab)
comment         -> ReviewVerdictWaitingForAuthor

The `comment` verdict deliberately maps to "waiting for author" rather than to a native COMMENT review: on Azure DevOps that lands as vote `-5` (the documented "waiting on the author" signal), and on GitHub gitforge translates `WaitingForAuthor` to `event=COMMENT` so the review surfaces without flipping the PR into a "Changes requested" hard block. This keeps the bot's "I have something to flag but no strong opinion" review visible in the platform's reviewer panel on every AI run.

The ok return is false only for verdicts the parser does not recognise — a corrupt LLM payload should not cause a spurious vote.

func ParseReviewResponse

func ParseReviewResponse(content string) (*entities.ReviewResult, error)

ParseReviewResponse parses an AI response string into a `ReviewResult`.

Strategy, in order:

  1. strict `json.Unmarshal` of the entire content;
  2. extract a fenced ```json ... ``` block and unmarshal that;
  3. run a repair pass that escapes unescaped double quotes inside string values, then unmarshal the repaired content;
  4. give up — log a length + content fingerprint at `ERROR`, log the raw content (truncated) at `DEBUG` only, and return `ErrUnparseableResponse` so the worker logs the failure and does not post anything to the PR.

Step 3 exists because LLMs occasionally forget to escape a `"` inside a generated string value (e.g. `"body":"... — "Always use ..."."`) and the stock parser then dropped the whole response into a PR thread as plain text. See `code-guru` PR review of `internal/auth-service#NNNN` thread `71418` for the canonical failure trace.

The raw content is intentionally NOT emitted at `ERROR` because the model echoes pieces of the prompt back in the body field and the prompt embeds the full PR diff — so an unconditional raw-content log would dump arbitrary repository source (and any in-diff secrets) into shared log stores. Operators who need the raw output for diagnosis can drop the log level to `DEBUG` on a single pod.

func SplitUnifiedDiff

func SplitUnifiedDiff(fullDiff string) map[string]string

SplitUnifiedDiff splits a multi-file unified diff into per-file chunks. Each returned element is keyed by the new-side file path (b/...) with its diff hunk.

func ThreadPromptID

func ThreadPromptID(index int) string

ThreadPromptID returns the synthetic per-prompt identifier the user prompt renders next to each prior thread (`T1`, `T2`, ...). Exported so the post-pipeline can rebuild the same ids in the same order to match the LLM's `thread_resolutions[].id` back to the conversation thread it refers to. Index is 0-based on the threads slice; the rendered id is 1-based for human readability.

func Truncate

func Truncate(s string, n int) string

Truncate returns the first n bytes of s plus a sentinel when the input is longer than n. Used for plain-text truncation that does not need to be log-injection safe (e.g., assembling a longer string for the operator).

The cut is byte-based — a trailing multi-byte rune may be split, which is acceptable for the diagnostic surface this helper targets. Callers that embed the result in a log line should use `TruncateForLog` / `TruncateBytesForLog` instead.

func TruncateBytesForLog

func TruncateBytesForLog(b []byte, n int) string

TruncateBytesForLog is the byte-slice variant of `TruncateForLog`. Critically, it converts only the first `min(n, len(b))` bytes to a string before quoting — which means a 50 MB request body never allocates a 50 MB intermediate string just to log the first 4 KB.

This matters on the webhook-handler diagnostic path because a hostile caller could otherwise amplify a single forbidden request into a proportional memory spike. Truncating the byte slice up-front keeps the cost bounded by `n + truncationSentinel`.

func TruncateForLog

func TruncateForLog(s string, n int) string

TruncateForLog returns a `strconv.Quote`d, single-line representation of the first n bytes of s, with the truncation sentinel appended (outside the quotes) when the input is longer than n.

Designed for the diagnostic-log path where the source is arbitrary untrusted content (model output, webhook bodies). `strconv.Quote` escapes newlines, tabs, ANSI sequences, and any non-printable byte — so a malicious input cannot inject a fake log line by including a `\n level=error msg="..."` sequence in its body. The output is also guaranteed to be valid UTF-8 even when the input is not.

Use `TruncateBytesForLog` instead when the source is already a `[]byte` — it avoids the full-body string copy that `string(b)` would force.

Types

type ParsedPRURL

type ParsedPRURL struct {
	ProviderType string
	Organization string
	Project      string // Azure DevOps only
	RepoName     string
	PRID         int
}

ParsedPRURL holds the components extracted from a pull request URL.

func ParsePullRequestURL

func ParsePullRequestURL(rawURL string) (*ParsedPRURL, error)

ParsePullRequestURL extracts provider, org, repo, and PR ID from a pull request URL. Delegates to gitforge's ParsePullRequestURL and converts the result to code-guru's ParsedPRURL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL