audit

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 29 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 MergeBoundaryAOIs added in v1.6.0

func MergeBoundaryAOIs(results []security.AOIScanResult, boundaryAOIs []security.AreaOfInterest) []security.AOIScanResult

MergeBoundaryAOIs inserts synthetic boundary-coverage AOIs into the per-file AOI scan results. AOIs are appended to the matching file's existing AreasOfInterest list (creating a new AOIScanResult when the file isn't in `results` already).

Returns the merged result list. Idempotency: if an AOI with the same ID already exists in the file, the synthetic AOI is dropped rather than duplicated. This keeps the merge safe to re-run on already-cached results.

func NeedsHierarchical

func NeedsHierarchical(findingCount int) bool

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

func OrderFilesByRecency added in v1.6.0

func OrderFilesByRecency(repoRoot string, files []string, lookback int) ([]string, error)

OrderFilesByRecency reorders files so paths touched in the last `lookback` commits come first (most-recent first), with the rest preserved in stable order behind them. Paths not touched recently are not dropped. When lookback ≤ 0, lookback falls back to recentLookbackDefault.

Best-effort: a `git log` failure is non-fatal — the original order is returned and the caller logs the warning.

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 RestrictToRecent added in v1.6.0

func RestrictToRecent(repoRoot string, files []string, lookback int) ([]string, error)

RestrictToRecent returns the subset of files that were touched in the last `lookback` commits, in most-recent-first order. Files not touched in the window are dropped entirely. When lookback ≤ 0, returns files unchanged (no restriction).

Best-effort: a `git log` failure is non-fatal — returns the input list unchanged.

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.

func SynthesizeBoundaryAOIs added in v1.6.0

func SynthesizeBoundaryAOIs(inventory []state.Boundary) []security.AreaOfInterest

SynthesizeBoundaryAOIs turns a boundary inventory into a slice of AreaOfInterest entries that ensure every boundary is reviewed for the standard defense classes. The AOIs are deterministic (no LLM) — for each boundary we emit one AOI per defense question appropriate to the kind.

The returned slice groups AOIs by File so the caller can merge them into per-file AOIScanResult records without re-sorting.

Types

type BoundaryDiscoveryResult added in v1.6.0

type BoundaryDiscoveryResult struct {
	// Boundaries is the parsed boundary list. Nil when loaded from
	// cache (caller already holds the cached value).
	Boundaries []state.Boundary

	// InputHash is a SHA-256 hash of the inputs used to generate the
	// inventory. Used for cache invalidation.
	InputHash string

	// FromCache indicates the cached hash matched; Boundaries is
	// nil in that case.
	FromCache bool
}

BoundaryDiscoveryResult holds the output of one Phase 1.5 pass.

func DiscoverBoundaries added in v1.6.0

func DiscoverBoundaries(
	ctx context.Context,
	client ai.Client,
	files map[string]string,
	runtimeModel *state.RuntimeModel,
	cachedHash string,
	onProgress func(string),
) (*BoundaryDiscoveryResult, error)

DiscoverBoundaries runs Phase 1.5: one LLM call that takes file header excerpts + the runtime model (when available) and emits a structured list of externally-reachable surfaces.

The runtime model is optional — passing nil falls back to "look at the file headers and classify what you see." Quality is better when the runtime model is present because its entry-point classes anchor the classification.

On LLM/parse failure returns an error; the caller treats Phase 1.5 as best-effort and proceeds without the inventory.

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

	// AuditRecent restricts the audit to files touched in the last
	// N commits (and only those — cold files are dropped entirely).
	// Zero = no restriction; the audit covers every file that passes
	// filtering. Either way, files touched recently are sorted to
	// the front so --max-reviews truncates AOIs in cold files first.
	AuditRecent int

	// SiblingClustering enables Phase 2.5: after Phase 2 AOI
	// generation, run an LLM call (or per-category calls when the
	// AOI set is large) that groups AOIs by shape and surfaces
	// outliers — handlers/call-sites whose pattern disagrees with
	// the majority of their siblings. Each outlier becomes an
	// individual-urgency AOI for Phase 3.
	//
	// Gated behind a flag for the first release so precision can be
	// measured on real audits before making it default.
	SiblingClustering bool

	// 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"`
	RecheckDismissals        []state.DismissedRecord `json:"recheck_dismissals,omitempty"`
	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

	// RecheckDismissals is the Phase 3b dismissal log — every finding
	// the recheck pass removed, with the rationale the model gave.
	// Separate from Dismissals (which counts Phase 3 AOI-level
	// dismissals before any finding was emitted); these are findings
	// that survived Phase 3 and were then dropped by recheck.
	RecheckDismissals []state.DismissedRecord

	// 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 SiblingClusterResult added in v1.6.0

type SiblingClusterResult struct {
	// Outliers is the list of synthesized outlier AOIs (urgency
	// "individual", SiblingDeviation populated) ready to merge into
	// the per-file scan results.
	Outliers []security.AreaOfInterest

	// InputHash is a SHA-256 hash of the AOI inputs + runtime model.
	InputHash string

	// FromCache indicates the cached hash matched; Outliers is nil
	// in that case (caller uses the cached value it already holds).
	FromCache bool
}

SiblingClusterResult holds the output of one Phase 2.5 pass.

func DiscoverSiblingOutliers added in v1.6.0

func DiscoverSiblingOutliers(
	ctx context.Context,
	client ai.Client,
	aois []security.AreaOfInterest,
	cachedHash string,
	onProgress func(string),
) (*SiblingClusterResult, error)

DiscoverSiblingOutliers runs Phase 2.5: it groups AOIs by shape via LLM clustering, identifies deviants whose pattern disagrees with their siblings, and synthesizes outlier AOIs (urgency individual, SiblingDeviation populated) for Phase 3.

Above siblingClusterGlobalThreshold total AOIs the work splits per category and runs in parallel — one LLM call per category — so no single prompt has to hold the full AOI set. Below the threshold a single global call gives the LLM cross-category visibility.

On LLM/parse failure for any single call this function logs and keeps going with whatever results other calls produced — sibling clustering is a quality enhancement, not a load-bearing input. Returns nil Outliers when no clusters meet the threshold.

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