audit

package
v1.4.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const MaxAuditFileBytes = 512 * 1024

MaxAuditFileBytes is the per-file size cap for audit ingestion. Files larger than this are skipped with a warning rather than loaded into memory and shipped to the classifier/AOI scanner — a single 10MB JSON config can balloon prompt sizes and offers no real audit value.

512KB comfortably accommodates hand-written code (the largest .go files in stdlib are ~300KB) while still catching generated artifacts and accidentally-committed data dumps. Audit.Options.MaxFileBytes overrides this at the call site.

Variables

This section is empty.

Functions

func BuildSynthesisUserMessage

func BuildSynthesisUserMessage(findings []state.DeepFinding, crossCutting []string, projectContext string, failedAOICount int) string

BuildSynthesisUserMessage formats findings and context into the user message sent to the LLM for synthesis.

failedAOICount > 0 appends a "## Audit Recall Gap" section asking the model to mention the gap in the executive summary. Synthesis otherwise can produce a confident-sounding summary on top of degraded inputs with no way for the reader to know.

func EstimateSynthesisChars

func EstimateSynthesisChars(findingCount int) int

EstimateSynthesisChars returns the expected total output character count for synthesis given the finding count. Used to drive a truthful streaming progress bar.

The estimate is bounded:

  • 3000 chars minimum (clean audit: executive_summary + 1-2 risks)
  • 10000 chars maximum (lots of findings hit the prompt's per-item word caps, so output stops growing past a point)

100 chars per finding is the slope: each finding contributes ~1 short risk/pattern/recommendation entry on average (≤100 words ≈ 400 chars per entry, but only a fraction of findings generate one).

The estimate intentionally errs HIGH so the streaming bar fills slower than reality — better to see a bar that holds at 95% for a moment than to claim 100% before the response actually ends.

func Export

func Export(result *Result, synthesis *SynthesisResult, path string) error

Export auto-detects format from file extension (.json or .md/.markdown). synthesis may be nil; the report is still written without the executive summary section in that case.

func ExportJSON

func ExportJSON(result *Result, synthesis *SynthesisResult, path string) error

ExportJSON writes the audit result as structured JSON to the given path. synthesis may be nil when synthesis was disabled or failed.

func ExportMarkdown

func ExportMarkdown(result *Result, synthesis *SynthesisResult, path string) error

ExportMarkdown writes a styled markdown report to the given path. synthesis may be nil when synthesis was disabled or failed.

func IsBinary

func IsBinary(content []byte) bool

IsBinary returns true if content appears to be a binary file. The heuristic: if any of the first binarySniffLimit bytes is NUL (0x00), treat as binary. Valid text encodings (UTF-8, ASCII, ISO-8859-*) never produce 0x00 in real content; binaries almost always have NUL bytes in headers, length-prefixed sections, or padding.

Known false positives: UTF-16/UTF-32 source files (vanishingly rare for code). Users can force-include via --include if needed.

func IsTransientUntracked

func IsTransientUntracked(path string) bool

IsTransientUntracked reports whether path looks like a locally generated artifact that should probably be in .gitignore. Only meaningful for files that came from `git ls-files --others`.

func LoadExcludeFile

func LoadExcludeFile(path string) ([]string, error)

LoadExcludeFile reads a glob-per-line exclude file (like .prr/audit-exclude). Blank lines and lines starting with # are skipped.

func LoadSnapshot

func LoadSnapshot(repoRoot string) ([]state.DeepFinding, error)

LoadSnapshot loads the previous audit's findings snapshot. Returns nil slice (not error) if no previous snapshot exists.

func NeedsHierarchical

func NeedsHierarchical(findingCount int) bool

NeedsHierarchical reports whether the given finding count exceeds the hierarchical synthesis threshold.

func RenderCategoryChart

func RenderCategoryChart(findings []state.DeepFinding) string

RenderCategoryChart returns a horizontal bar chart of findings by category.

func RenderSeverityBar

func RenderSeverityBar(findings []state.DeepFinding) string

RenderSeverityBar returns a single proportional bar showing severity distribution.

func RunPlain

func RunPlain(
	ctx context.Context,
	reviewClient ai.Client,
	aoiClient ai.Client,
	opts Options,
	reviewModel, aoiModel string,
	noSynthesis bool,
) (*Result, *SynthesisResult, error)

RunPlain executes the audit without the Bubble Tea UI — prints progress to stderr. Used in --debug mode so debug output isn't clobbered by terminal animations.

func RunWithUI

func RunWithUI(
	ctx context.Context,
	reviewClient ai.Client,
	aoiClient ai.Client,
	opts Options,
	reviewModel, aoiModel string,
	noSynthesis bool,
) (*Result, *SynthesisResult, error)

RunWithUI executes the audit with the shared progress TUI. Returns the audit result and synthesis after completion.

func SaveSnapshot

func SaveSnapshot(repoRoot string, findings []state.DeepFinding) error

SaveSnapshot persists current findings as the "last audit" baseline. Stored in .git/pr-tui/audit-snapshot.json

func SetHierarchicalSynthConcurrency

func SetHierarchicalSynthConcurrency(n int)

SetHierarchicalSynthConcurrency sets the max number of per-category synthesis calls run in parallel. Values <= 0 reset to the default. Not safe to call concurrently with synthesis in flight.

func ShouldExcludeFromAudit

func ShouldExcludeFromAudit(path string) bool

ShouldExcludeFromAudit returns true if the given file path should be excluded from audit mode review. Checks both the standard review exclusions and the audit-specific exclusions.

func ShouldExcludeFromAuditWithCustom

func ShouldExcludeFromAuditWithCustom(path string, customPatterns []string) bool

ShouldExcludeFromAuditWithCustom checks against standard, audit, and user-provided custom exclusion patterns.

func ShouldForceInclude

func ShouldForceInclude(path string, includePatterns []string) bool

ShouldForceInclude returns true if the path matches any --include pattern. Force-included files override exclusion filters.

Types

type CollectStats

type CollectStats struct {
	// TotalListed is the count of paths returned by `git ls-files`
	// (tracked + untracked-non-ignored) before any filter ran.
	TotalListed int

	// Included is the count of paths that survived all filters.
	// Equal to len(files) returned by CollectFiles.
	Included int

	// Tracked / Untracked break Included down by git origin.
	Tracked   int
	Untracked int

	// Exclusion counts, ordered by the precedence applied in
	// partitionFiles. Each path increments exactly one of these (or
	// makes it into Included).
	ExcludedReview int // standard review exclusions (lock/vendor/generated/etc.)
	ExcludedAudit  int // audit-specific exclusions (docs/build/IDE/etc.)
	ExcludedCustom int // user-provided --exclude patterns

	// ForceIncluded counts paths that matched --include and bypassed
	// exclusion. Subset of Included.
	ForceIncluded int

	// UntrackedTransients lists untracked files that survived
	// filtering AND look like locally generated artifacts (logs,
	// debug dumps, state snapshots). The caller surfaces a warning
	// suggesting .gitignore updates — we do NOT auto-exclude because
	// a legitimately-named file could match these patterns.
	UntrackedTransients []string
}

CollectStats summarizes what CollectFiles saw and dropped. Exposed so callers can surface drop reasons in --debug and warn the user about untracked files that look like local tooling output.

func CollectFiles

func CollectFiles(repoRoot string, excludePatterns, includePatterns []string) ([]string, CollectStats, error)

CollectFiles returns the list of files to audit after applying all filters, plus a CollectStats breakdown of what was dropped and why. Runs two `git ls-files` queries so we can distinguish tracked from untracked origin (needed for the transient-file warning).

type CompareResult

type CompareResult struct {
	New        []state.DeepFinding // findings in current but not previous
	Resolved   []state.DeepFinding // findings in previous but not current
	Persistent []state.DeepFinding // findings in both (may have changed severity etc)
}

CompareResult holds the diff between two audit runs.

func CompareFindings

func CompareFindings(current, previous []state.DeepFinding) CompareResult

CompareFindings compares current findings against a previous set. Matching is done by (File, Category, Subcategory, Title) tuple — line numbers may shift between runs.

func (CompareResult) FormatComparison

func (c CompareResult) FormatComparison() string

FormatComparison returns a human-readable summary like: "3 new findings, 2 resolved, 10 persistent"

type ConcurrencyConfig

type ConcurrencyConfig struct {
	// Classify caps Phase 1b classification calls.
	Classify int
	// AOIScan caps Phase 2 AOI batch calls.
	AOIScan int
	// DeepReview caps Phase 3 review calls.
	DeepReview int
	// Recheck caps Phase 3b recheck batch calls.
	Recheck int
	// HierarchicalSynth caps Phase 4 per-category synthesis calls.
	HierarchicalSynth int
}

ConcurrencyConfig holds per-phase concurrency caps. Each field is the maximum number of in-flight LLM calls for that phase. Zero = use the package default.

type CostEstimate

type CostEstimate struct {
	TotalCalls      int
	IndividualCalls int
	GroupedCalls    int
	EstInputTokens  int // rough estimate of total input tokens
	EstOutputTokens int // rough estimate of total output tokens
	EstCostUSD      float64
	SkippedCalls    int // calls that would be skipped by --max-reviews
}

CostEstimate holds pre-execution cost projections for Phase 3.

func EstimateCost

func EstimateCost(routing *review.RouteResult, maxReviews int, pricing ModelPricing) CostEstimate

EstimateCost projects Phase 3 costs from routing results.

func (CostEstimate) FormatEstimate

func (e CostEstimate) FormatEstimate() string

FormatEstimate returns a human-readable cost summary.

type ModelPricing

type ModelPricing struct {
	InputPerMTok  float64
	OutputPerMTok float64
	ModelName     string
}

ModelPricing holds per-1M-token costs.

func DefaultPricing

func DefaultPricing(modelName string) ModelPricing

DefaultPricing returns pricing for a model, looking up from the known models registry.

type OnProgress

type OnProgress func(phase string, message string)

OnProgress is called with status updates during the audit.

type Options

type Options struct {
	// RepoRoot is the absolute path to the repository root.
	RepoRoot string

	// Focus is a list of dimension slugs to focus on. Empty = all.
	Focus []string

	// ExcludePatterns are additional globs to exclude (from --exclude and .prr/audit-exclude).
	ExcludePatterns []string

	// IncludePatterns are globs to force-include (from --include).
	IncludePatterns []string

	// MaxReviews caps the number of Phase 3 review calls. 0 = no limit.
	MaxReviews int

	// NoCache disables incremental caching — re-audit everything.
	NoCache bool

	// AOIContextLines is the number of context lines for AOI generation.
	AOIContextLines int

	// Debug enables verbose output of all LLM prompts and responses.
	Debug bool

	// DebugFile restricts the audit to a single file (path relative to repo root).
	DebugFile string

	// LargeFileThreshold is the file count above which a warning is
	// displayed. Defaults to 200 when zero.
	LargeFileThreshold int

	// MaxFileBytes caps per-file size during Phase 1 ingestion. Files
	// exceeding the cap are skipped with a warning (not loaded into
	// prompts). Zero falls back to MaxAuditFileBytes.
	MaxFileBytes int

	// Concurrency tunes per-phase concurrency caps. Each zero field falls
	// back to the package default (currently 5).
	Concurrency ConcurrencyConfig
}

Options configures an audit run.

type PhaseUsage

type PhaseUsage struct {
	AOI     ai.TokenUsage // Phase 2: AOI pre-scan
	Review  ai.TokenUsage // Phase 3: deep review
	Recheck ai.TokenUsage // Phase 3b: recheck/dedup
	Synth   ai.TokenUsage // Phase 4: synthesis
}

PhaseUsage holds per-phase token usage for cost reporting.

func (PhaseUsage) Total

func (u PhaseUsage) Total() ai.TokenUsage

Total returns aggregate token usage across all phases.

type ReportJSON

type ReportJSON struct {
	FilesScanned             int                 `json:"files_scanned"`
	AOIsGenerated            int                 `json:"aois_generated"`
	ReviewCalls              int                 `json:"review_calls"`
	IndividualReviews        int                 `json:"individual_reviews"`
	GroupedReviews           int                 `json:"grouped_reviews"`
	FailedReviews            int                 `json:"failed_reviews,omitempty"`
	Findings                 []state.DeepFinding `json:"findings"`
	Dismissals               int                 `json:"dismissals"`
	CrossCuttingObservations []string            `json:"cross_cutting_observations,omitempty"`
	SkippedSubcategories     []string            `json:"skipped_subcategories,omitempty"`
	Synthesis                *SynthesisResult    `json:"synthesis,omitempty"`
}

ReportJSON is the JSON-serializable form of an audit result.

type Result

type Result struct {
	// FilesScanned is the number of files that passed Phase 1 filtering.
	FilesScanned int

	// AOIsGenerated is the total number of AOIs from Phase 2.
	AOIsGenerated int

	// ReviewCalls is the number of Phase 3 LLM calls made.
	ReviewCalls int

	// IndividualReviews is the count of individual review calls.
	IndividualReviews int

	// GroupedReviews is the count of grouped review calls.
	GroupedReviews int

	// FailedReviews is the count of Phase 3 review calls that errored and
	// produced no findings. The user should know about these — silently
	// dropping them means the audit's recall is lower than it appears.
	FailedReviews int

	// FailedAOIIDs is the list of AOI IDs whose Phase 3 review call
	// failed (and therefore produced no finding or dismissal). Carries
	// the per-call FailedAOIIDs from review.ExecuteResult so synthesis
	// can mention recall gaps in the executive summary and the reporter
	// can name exactly which areas of the codebase didn't get reviewed.
	FailedAOIIDs []string

	// Findings is all confirmed findings from Phase 3.
	Findings []state.DeepFinding

	// Dismissals is all dismissed AOIs from Phase 3.
	Dismissals int

	// CrossCuttingObservations from grouped reviews.
	CrossCuttingObservations []string

	// SkippedSubcategories lists subcategories skipped due to --max-reviews.
	SkippedSubcategories []string

	// Routing is the Phase 3 routing result (for summary display).
	Routing *review.RouteResult

	// ProjectContext is the Phase 0 project briefing. Captured here so the
	// caller can plumb it into Phase 4 synthesis (otherwise the work done in
	// Phase 0 would not reach the executive summary).
	ProjectContext string

	// TokenUsage tracks actual token consumption per phase.
	Usage PhaseUsage
}

Result holds the output of an audit run.

func Run

func Run(
	ctx context.Context,
	reviewClient ai.Client,
	aoiClient ai.Client,
	opts Options,
	onProgress OnProgress,
) (*Result, error)

Run executes the full audit pipeline (Phases 0-3). Phase 4 (synthesis) is handled separately by the caller.

type SynthesisResult

type SynthesisResult struct {
	// ExecutiveSummary is a 2-3 paragraph overview of audit findings.
	ExecutiveSummary string `json:"executive_summary"`

	// TopRisks are the most critical issues identified, ranked.
	TopRisks []string `json:"top_risks"`

	// SystemicPatterns are recurring issues across the codebase.
	SystemicPatterns []string `json:"systemic_patterns"`

	// Recommendations are prioritized action items.
	Recommendations []string `json:"recommendations"`

	// RawOutput is the full LLM response.
	RawOutput string `json:"-"`
}

SynthesisResult holds the output of Phase 4 synthesis.

func ParseSynthesisResult

func ParseSynthesisResult(raw string) (*SynthesisResult, error)

ParseSynthesisResult extracts a SynthesisResult from the LLM's raw response. Uses ai.ExtractJSON so trailing prose, embedded fences, and multi-round agent output don't break parsing.

Returns errSynthesisParse-wrapped errors on parse-shape failures so callers (and the retry wrapper) can distinguish "model output was malformed" from "transport error" — the former won't be fixed by retrying the same prompt.

func Synthesize

func Synthesize(
	ctx context.Context,
	client ai.Client,
	findings []state.DeepFinding,
	crossCutting []string,
	projectContext string,
	failedAOICount int,
	onToken func(string),
) (*SynthesisResult, error)

Synthesize runs Phase 4: takes all findings and produces an executive summary. onToken is called for streaming output (can be nil).

failedAOICount is the number of AOIs whose Phase 3 review failed (so they have no finding/dismissal verdict). When > 0, synthesis is told to mention the recall gap in the executive summary — otherwise the user reads a confident summary on top of degraded inputs and has no way to know.

func SynthesizeCached

func SynthesizeCached(
	ctx context.Context,
	client ai.Client,
	findings []state.DeepFinding,
	crossCutting []string,
	projectContext string,
	failedAOICount int,
	onToken func(string),
	noCache bool,
) (*SynthesisResult, error)

SynthesizeCached wraps Synthesize with persistent cache lookup against the audit state. Cache misses run synthesis and persist the result. Cache hits are returned without an LLM call. Pass noCache=true to bypass.

The audit state is loaded and (if updated) saved by this function — callers don't need to manage it.

Jump to

Keyboard shortcuts

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