support

package
v0.0.0-...-4e341e7 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const MentionToken = "@code-guru"

MentionToken is the built-in 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.

This is the FLOOR, not the whole trigger set: HasMention additionally accepts mentions of the account the deployment actually posts under, derived from `Settings.BotIdentities` (see mentionTokensFor).

Variables

View Source
var ErrArgumentListTooLong = errors.New("ai prompt exceeds the operating system argument limit")

ErrArgumentListTooLong marks a review failure whose root cause is that the assembled prompt could not be HANDED to a subprocess backend: the operating system refused the exec because one argument exceeded its per-argument limit (`E2BIG`).

On Linux that limit is `MAX_ARG_STRLEN` — 32 pages, i.e. 128 KiB — and it caps a SINGLE argv string. It is independent of the far larger `ARG_MAX` total (2 MiB in a typical container), so an operator who measures `getconf ARG_MAX` and concludes there is plenty of headroom is reading the wrong number. It is a compile-time kernel constant; no ulimit moves it.

It is its own failure class for the same two reasons the other sentinels are:

  • It is DETERMINISTIC. The prompt is byte-for-byte identical on every attempt, so the kernel refuses the re-sample identically — the retry decorator short-circuits it instead of burning the attempt budget.
  • It has a DIFFERENT remedy, and one the PR author cannot apply. Nothing about the diff caused it: the oversized argument is the SYSTEM prompt, assembled from the operator's rule corpus. It clears when the operator shrinks the configured rules (`rules.categories`), not when the author pushes a commit — which is exactly what the generic annotation would otherwise tell them to do.

The Claude CLI backend passes its system prompt via `--system-prompt-file` precisely so this cannot happen. The sentinel covers the fallback path (a temp file that could not be created) and any future subprocess backend.

View Source
var ErrContentSafetyRefusal = errors.New("ai content-safety refusal")

ErrContentSafetyRefusal is the sentinel identifying a review failure whose root cause is that the AI backend's content-safety system DECLINED to review the pull request — most commonly for security-related code (offensive security, exploit or credential-handling code, cryptography) and, for some models, certain biology content.

Like ErrContextWindowExceeded it is a distinct class with its own handling:

  • It is DETERMINISTIC on the same content — re-sampling the identical diff is declined the same way, so the retry decorator short-circuits it instead of burning the attempt budget on a review that cannot succeed.
  • It has a DIFFERENT remedy. It is not a defect in the PR and not fixable by pushing a commit; the review clears only with a human review, or by pointing the reviewer at a model/backend whose safety classifiers cover the content. The command layer renders a dedicated annotation saying so.

Match it with errors.Is; extract the provider's classification label with errors.As against a ContentSafetyRefusalError.

View Source
var ErrContextWindowExceeded = errors.New("ai prompt exceeds the model context window")

ErrContextWindowExceeded marks a review failure whose root cause is that the assembled prompt (the PR diff plus rules, guidelines, metadata, and conversation) is larger than the AI model's context window — i.e. the pull request is simply too big to review in a single pass.

It is a distinct class from a transient backend hiccup or an unparseable response for two reasons:

  • It is DETERMINISTIC. The prompt is identical on every attempt, so a re-sample fails identically — the retry decorator short-circuits it instead of burning the whole attempt budget (and, on paid backends, the cost) on a review that cannot succeed until the PR shrinks.
  • It has a DIFFERENT remedy. A transient failure clears on retry; this one clears only when the author makes the change smaller. The command layer renders a dedicated, actionable annotation for it (split the PR, exclude generated/lock files) instead of the generic "usually transient — push a new commit" message, which is actively wrong here: pushing more commits only grows the diff.

Each backend wraps its provider-specific "prompt too long" error with this sentinel (via LooksLikeContextWindowError) so the command layer can classify the failure with a single errors.Is check, regardless of which backend hit the limit.

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.

On a retry attempt (`request.Attempt > 1`) the `retryJSONReminder` is appended so the re-sample is nudged back to bare-JSON output. The first attempt (`Attempt` 0 or 1) is byte-for-byte identical to the pre-retry prompt, so the no-drift guarantee for normal reviews still holds.

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 BuildUserPromptFor

func BuildUserPromptFor(request entities.ReviewRequest) string

BuildUserPromptFor assembles the user prompt from the full review request. All AI backends share this helper — the same reasoning as `BuildSystemPromptFor`: the request-derived sections (PR metadata, project guidelines, prior conversation) must be assembled in exactly one place, or a future backend could silently drop one of them and drift the review quality of a single backend without a compile error.

A request with no `Metadata`, no `ProjectGuidelines`, and no `Conversation` produces output byte-for-byte identical to `BuildUserPrompt`, preserving the no-drift guarantee for reviews that carry none of those sections.

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 DetectBotAuthors

func DetectBotAuthors(comments []forgeEntities.PullRequestComment) []string

DetectBotAuthors returns the distinct set of comment authors that posted one of the bot's PR-wide status annotations on this PR (the "reviewing" / "review complete" / "review failed" notices, all of which carry the botAnnotationMarker substring).

It exists so the re-review conversation walk can recognise the bot's own prior comments REGARDLESS of the account name the deployment posts under. On GitHub the bot is `code-guru[bot]` and the built-in matcher in `IsBotAuthor` recognises it, but a self-hosted Azure DevOps deployment commonly posts under an organisation service account (e.g. an `automation` / `svc-*` identity) whose name does not start with `code-guru`. Without self-detection, `BuildReviewConversation` finds zero prior bot threads, the LLM re-reviews from scratch on every re-review, and the bot re-posts findings the PR author has already addressed or rebutted.

Only PR-wide comments (`Line <= 0`) carrying the annotation marker are considered, so a human who merely quotes the bot inline is not mis-identified. The returned identities are suitable to pass straight to `IsBotAuthor` as additional bot identities. Order is the first-seen order of the input; duplicates are collapsed.

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, identities ...string) bool

HasMention returns true when the comment body mentions the bot. Two kinds of mention count:

  1. the built-in `@code-guru` token (always accepted, so a deployment with no configuration keeps working exactly as before); and
  2. any account the deployment posts under, supplied by the caller from `Settings.BotIdentities` and normalised by mentionTokensFor.

The second form exists because a deployment posting under a GitHub App (`code-guru[bot]`) or an organisation service account (`svc-codeguru@corp.example`) is @-mentioned by the name users see on the PR — which used to match nothing at all, leaving the bot silent.

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).

func IsBotAuthor

func IsBotAuthor(identities ...string) func(string) bool

IsBotAuthor returns a predicate the conversation walker uses to identify the bot's own comments. An author is the bot when EITHER:

  1. it exactly (case-insensitively) equals one of the `identities` the caller supplies — the account the deployment posts under (a service-account login / email), wired from `Settings.BotIdentities` and/or self-detected from the bot's own PR-wide status annotations (see `DetectBotAuthors`); OR
  2. it matches the built-in `code-guru` name shape (the default, always recognised so GitHub App deployments need no config).

The built-in shape match is **strict** so user names that merely 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. It 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>`)

Configured identities are matched by EXACT (case-insensitive) full equality rather than the prefix rule: they are complete account identities, so a substring/prefix match would risk pulling an unrelated user with a coincidentally overlapping name into the bot's conversation context.

Returned as a closure (rather than a free function) so the caller can combine configured + self-detected identities per re-review without touching the assembler. Passing no identities preserves the pre-configuration behaviour byte-for-byte.

func LooksLikeArgumentListTooLong

func LooksLikeArgumentListTooLong(err error) bool

LooksLikeArgumentListTooLong reports whether an exec failure is the OS `E2BIG` condition, so a subprocess backend can wrap it with ErrArgumentListTooLong.

Unlike LooksLikeContextWindowError this needs no string matching: the condition is raised by the kernel, not phrased by a provider. `os/exec` surfaces it as a `*fs.PathError` wrapping `syscall.E2BIG` (rendered as "fork/exec <binary>: argument list too long"), which errors.Is unwraps directly. `syscall.E2BIG` is defined on every platform Go supports — including Windows — so this needs no build tag.

func LooksLikeContextWindowError

func LooksLikeContextWindowError(msg string) bool

LooksLikeContextWindowError reports whether a backend error message names a context-window / prompt-too-long failure. Backends call it on their raw error text to decide whether to wrap the error with ErrContextWindowExceeded. The match is case-insensitive and substring-based so it survives the varying envelopes each provider wraps the core message in.

The marker set is deliberately conservative: matching a genuinely transient error here would wrongly suppress retries and post the "too large" guidance for a blip. Every marker names input/context size specifically, never an output (max_tokens) cap. The shapes covered are:

  • Anthropic Messages API 400: "prompt is too long: N tokens > M maximum"
  • OpenAI chat completions: "This model's maximum context length is ..." (error code "context_length_exceeded")
  • Claude CLI: wraps the Anthropic "prompt is too long" text

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 ParseContextWindowOverage

func ParseContextWindowOverage(msg string) (int, int, bool)

ParseContextWindowOverage extracts the "how much too big was it" figures from a backend's prompt-too-long error, returning the token count the prompt used and the model's limit. `ok` is false when the message carries no usable pair, which is the normal case for backends that phrase the failure without numbers — callers must always have a fallback.

It exists so the batching fallback can size its FIRST batch from the real overage instead of blindly halving. On a pull request several times the window, halving costs one wasted round trip per level — and every one of those uploads the whole multi-megabyte prompt again before the API rejects it. Reading `used` and `limit` off the error turns that search into a single step.

The shapes covered (both are `used > limit`, which is also the sanity check applied before returning):

  • Anthropic: `prompt is too long: 1234567 tokens > 200000 maximum`
  • OpenAI: `This model's maximum context length is 128000 tokens. However, your messages resulted in 130000 tokens.`

Note the two providers order the figures differently, so the pair is derived positionally-agnostically: the largest matched figure is what the prompt used, the smallest is the limit it blew past.

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 ContentSafetyRefusalError

type ContentSafetyRefusalError struct {
	Category string
}

ContentSafetyRefusalError is the typed error backends return for a content-safety refusal. Category carries the provider's classification label for the policy that fired ("cyber", "bio", ...) or "" when the provider does not report one. The label is a coarse, provider-assigned classification — never raw model output — so the command layer may safely surface it in the annotation.

func (*ContentSafetyRefusalError) Error

func (e *ContentSafetyRefusalError) Error() string

Error implements the error interface.

func (*ContentSafetyRefusalError) Is

func (e *ContentSafetyRefusalError) Is(target error) bool

Is lets errors.Is match any ContentSafetyRefusalError against ErrContentSafetyRefusal regardless of its Category, so callers classify the failure with a single check while still being able to errors.As it for the category.

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