Skip to content

feat: server-side blast radius for PR reviews - #130

Merged
LinceMathew merged 10 commits into
masterfrom
blast-radius
Oct 10, 2026
Merged

LinceMathew merged 10 commits into
masterfrom
blast-radius

Conversation

@LinceMathew

Copy link
Copy Markdown
Contributor

Summary

PR reviews started from LiveReview (UI, API, MCP, PR list) now get a blast-radius report, not just git lrc review runs. A River job runs alongside the AI review and saves through the existing artifact path, so the diff viewer, Math Mode and blast_radius_hunks (Livi) work unchanged.

Design: docs/blast-radius-server-side-plan.md.

How it works

  • Repo cache (internal/blastradius/repocache): blobless, ~13-month shallow clone per repo under lrdata/blastradius/<org>/<connector>/<owner>__<repo>. Later reviews only fetch the PR ref. HTTPS + the connector's token, passed as a header via env: never in argv, .git/config or logs. Token only sent to the connector's own host, no redirects. Diff is plain git diff -M <merge-base> <head>, no provider API.
  • Scoring (internal/blastradius/serverscore): codebase-memory-mcp index + git-lrc's engine imported as a Go module (github.com/HexmosTech/git-lrc/blastradius v0.1.0, blastradius: rename module to github.com/HexmosTech/git-lrc/blastradius git-lrc#145). Same engine as the CLI, so scores match.
  • Job (internal/jobqueue/blast_radius_worker.go): queued in QueueManualReviewJob, own blast_radius queue (1 worker), 15 min timeout, best-effort, never affects the review. All queries scoped by org_id. Skips CLI, scheduled and non-PR reviews.
  • Settings → Storage → Blast Radius Repo Cache: on/off (default on), cache size (default 5 GB, min 1 GB), usage. system_settings row blast_radius_cache; saving rejects API keys. Mega-menu entry added.
  • Cache limits: LRU eviction (repo + its graph index) after each job. A repo bigger than the cache is marked too-large and later reviews show a "No blast radius" notice, with an "Increase cache size" link for owners/admins. Under 1 GB free disk → skipped with a notice. Connector/org deletion removes their cached repos.
  • Review page: re-checks for the report every 30s (up to 15 min) since it can land after the findings.

Docker / deps

  • codebase-memory-mcp pinned to v0.9.0 and locked in PINNED_DOCKER_DEPS. git-lrc's engine parses v0.9.0's JSON; v0.10.x prints text even with --json.
  • git added to the runtime image (it was missing) and to verify_docker_deps.sh.
  • Go 1.26.9 (go.mod + GOLANG_IMAGE_TAG=1.26.9-bookworm).

Testing

  • Local, GitHub (LinceMathew/ragfolio): clone → index → score → badges on the review page; cache files, index and usage verified.
  • Local, GitLab (gitlab.com/Amazing-Stardom/painlessMesh): full path works. That MR only touched .ino files, which codebase-memory-mcp doesn't index, so its scores are 0.
  • Repo cache tested against public/private GitHub and GitLab repos (shallow window, inactive repos, old PR bases, token not on disk).
  • go build/go vet, npx tsc --noEmit, docker buildx build --check on both Dockerfiles, check_docker_deps.py --check.

Not yet verified

  • Full Docker image build + make verify-docker-deps.
  • Bitbucket, Gitea, Azure DevOps.

Deploy notes

Hosts running the worker need git and codebase-memory-mcp v0.9.0 on the worker's PATH (Docker image has both). Otherwise jobs log and skip; reviews are unaffected.

Also included: db/schema.sql dump and .synced-commits.env updates from local tooling.

LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%)
LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%)
… cache settings)

LiveReview Pre-Commit Check: ran
@LiveReview-Bot

Copy link
Copy Markdown

Implement Server-Side Blast Radius Calculation

Overview

This change adds server-side blast radius scoring for code reviews. It introduces a new repository caching mechanism and integrates it with a background job queue. Users now receive automated blast radius scores directly within the review interface.

Technical Highlights

  • internal/blastradius/repocache/repocache.go: Manages a local blobless git clone cache for PR review checkouts.
  • internal/jobqueue/blast_radius_worker.go: Implements a River background worker to clone repositories and compute blast radius scores.
  • internal/api/server.go, ui/src/pages/Settings/BlastRadiusCacheSettingsTab.tsx: Add administrative API routes and a new UI tab for managing blast radius cache settings.
  • Dockerfile, go.mod: Update Go version, pin codebase-memory-mcp to v0.9.0, and add git-lrc blastradius module dependency.
  • db/schema.sql: Updates river_job table and adds upgrade_request_events for state tracking.

Impact

  • Functionality: Provides automated server-side blast radius scores for code reviews, including UI feedback for skipped calculations.
  • Risk: Requires significant lrdata volume disk space (5 GB minimum) and introduces new external Go and system dependencies.

func MarkTooLarge(root string, c *Checkout, size int64) error {
entry := filepath.Dir(c.Dir)
removeEntry(root, entry)
repoLocks.Delete(c.Dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

There is a lock key mismatch in MarkTooLarge. The repoLocks.Delete function uses c.Dir instead of entry/repo.

Suggestions:

  1. Use consistent lock key construction matching removeIfIdle (filepath.Join(c.Dir, 'repo'))

}
dir := filepath.Join(base, "repo")

mu, _ := repoLocks.LoadOrStore(dir, &sync.Mutex{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The repoLocks sync.Map grows unboundedly, which will leak memory over time.

Suggestions:

  1. Implement a cleanup or LRU mechanism for repoLocks
  2. Use a bounded cache or lock striping strategy

if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
return os.Setenv("CBM_CACHE_DIR", dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

os.Setenv mutates the global process environment. Concurrent requests may race on CBM_CACHE_DIR.

Suggestions:

  1. Pass the cache directory via command-line arguments or process execution environment instead of global os.Setenv
  2. Use a mutex or pool if process-wide env is unavoidable

}
})
.catch(() => {
if (!cancelled && ++attempts < 30) timer = setTimeout(load, 30000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

There is an unhandled catch rejection if the async flow fails repeatedly. Consider adding an error boundary or state logging.

Suggestions:

  1. Capture and log polling errors after max attempts are exhausted

min={settings.min_gb}
max={10000}
value={settings.max_gb}
onChange={(e) => setSettings({ ...settings, max_gb: parseInt(e.target.value, 10) || 0 })}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

parseInt is being used without a radix parameter. Please specify the radix 10 explicitly.

Suggestions:

  1. Pass radix 10 to parseInt

@LiveReview-Bot

Copy link
Copy Markdown

Implement Server-Side Blast Radius Scoring

Overview

This change introduces server-side blast radius calculation for code reviews. It leverages a new background job system and a local repository cache. This enables automated risk assessment for UI and API-triggered reviews.

Technical Highlights

  • AGENTS.md / docs/blast-radius-server-side-plan.md: Documents the new server-side blast radius architecture and background job integration.
  • Dockerfile / Dockerfile.crosscompile: Pins codebase-memory-mcp to v0.9.0 and adds git for repository operations.
  • db/schema.sql: Adds upgrade_request_events table and updates River job table schema.
  • internal/blastradius/repocache/: Implements local repository caching with LRU eviction and disk space management.
  • internal/jobqueue/blast_radius_worker.go: Adds a new River worker for asynchronous blast radius computation.
  • internal/api/storage_settings.go: Introduces API endpoints for managing blast radius cache settings.
  • internal/api/diff_review.go / storage/blastradius/store.go: Refactors blast-radius report persistence and parsing.
  • internal/diffutil/diffutil.go: Adds a shared utility function for parsing unified diff hunks.

Impact

  • Functionality: The system now generates blast radius reports for reviews initiated via UI or API.
  • Risk: Pinning codebase-memory-mcp to an older version may create dependency conflicts. New disk space requirements and background processes add operational overhead.

return 0, err
}
var st syscall.Statfs_t
if err := syscall.Statfs(root, &st); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

The use of syscall.Statfs is platform-specific and will break compilation on Windows.

Suggestions:

  1. Use build tags to provide a non-Unix implementation or use a cross-platform filesystem stats library.

// paths so they match the lock keys Prepare uses.
func entries(root string) []string {
root, _ = filepath.Abs(root)
matches, _ := filepath.Glob(filepath.Join(root, "*", "*", "*"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The glob pattern assumes a fixed 3-level directory depth, making it fragile if the directory structure changes.

Suggestions:

  1. Replace globbing with filepath.WalkDir to robustly discover cache entries regardless of depth.


func dirSize(dir string) int64 {
var size int64
_ = filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

filepath.WalkDir follows symbolic links. A malicious repository could potentially escape the cache root.

Suggestions:

  1. Check path prefixes or avoid following symlinks when calculating directory size.


// repoLocks: <entry>/repo -> *sync.Mutex. Bounded by the cached repos: a key is deleted
// when its repo leaves the cache (removeIfIdle, MarkTooLarge).
var repoLocks sync.Map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Entries in repoLocks are never deleted, which will lead to unbounded memory growth over time.

Suggestions:

  1. Remove entries from repoLocks when repository cache is evicted or pruned

if abs, err := filepath.Abs(indexDir); err == nil {
indexDir = abs
}
_ = os.Setenv("CBM_CACHE_DIR", indexDir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Calling os.Setenv within an init function can cause race conditions when running unit tests in parallel.

Suggestions:

  1. Pass cache directory configuration via options or function parameters instead of global env mutation

LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
@LiveReview-Bot

Copy link
Copy Markdown

Implement Server-Side Blast Radius Scoring

Overview

This change introduces server-side blast radius scoring for Pull Request (PR) reviews. It shifts computation from client-side to a new asynchronous worker architecture. This includes a robust Git repository caching system and integration with an external blast radius engine.

Technical Highlights

  • internal/blastradius/repocache/repocache.go: Implements a secure, blobless Git repository caching system for server-side analysis.
  • internal/blastradius/serverscore/serverscore.go: Orchestrates blast radius scoring by integrating the repository cache and codebase-memory-mcp engine.
  • internal/jobqueue/blast_radius_worker.go: Adds a background worker for asynchronous blast radius score computation.
  • db/schema.sql: Updates database schema to support new job queue tables and jsonb chat context.
  • go.mod: Adds a new external dependency, github.com/HexmosTech/git-lrc/blastradius v0.1.0.
  • Dockerfile, docker/docker-deps.env: Pins codebase-memory-mcp to v0.9.0 and adds git as a runtime dependency.
  • internal/api/storage_settings.go: Exposes API endpoints for viewing and updating blast radius cache configuration.
  • internal/api/connectors.go, internal/api/organizations/org_handlers.go: Adds asynchronous cleanup of cached repositories upon connector or organization deletion.
  • internal/diffutil/diffutil.go: Centralizes unified diff parsing logic, improving modularity.

Impact

  • Functionality: Adds asynchronous server-side blast radius scoring for PR reviews, with configurable cache settings.
  • Risk: Introduces new disk space requirements (minimum 5 GB) and potential race conditions in multi-worker cache access without multi-process locking.

}
defer mu.(*sync.Mutex).Unlock()
removeEntry(root, entry)
repoLocks.Delete(key) // the map only holds cached repos; a new job gets a fresh lock

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

The lock is deleted from repoLocks before releasing the mutex. This allows for concurrent acquisition of a new mutex for the same path.

Suggestions:

  1. Remove repoLocks.Delete from removeIfIdle or protect map cleanup with a global mutex


// repoLocks: <entry>/repo -> *sync.Mutex. Bounded by the cached repos: a key is deleted
// when its repo leaves the cache (removeIfIdle, MarkTooLarge).
var repoLocks sync.Map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

Multi-process workers require flock or an external locking mechanism. The current sync.Mutex only provides protection within a single process.

Suggestions:

  1. Implement flock or another cross-process locking mechanism if multiple worker processes can access the same cache directory concurrently.
  2. Document the current limitation and future plan for multi-process access.

return err
}
// Rename detection in the graph engine's git log would lazily fetch old blobs one by one.
for _, kv := range [][2]string{{"diff.renames", "false"}, {"checkout.workers", "8"}} {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Setting diff.renames=false conflicts with the Diff method, which uses git diff -M for rename detection.

Suggestions:

  1. Clarify intent: if git diff -M is used, diff.renames should typically be true or left at default. If the graph engine handles renames, this git config might be irrelevant or intended to prevent Git from doing its own rename detection for other commands. Add a comment explaining this decision.
  2. If git diff -M is meant to detect renames, remove {"diff.renames", "false"}.

LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
@LiveReview-Bot

Copy link
Copy Markdown

Implement Server-Side Blast Radius Scoring and Repository Caching

Overview

Adds a server-side background worker to compute code blast radius for pull requests. Integrates repository cloning, persistent disk caching, LRU eviction, and configuration APIs.

Technical Highlights

  • internal/jobqueue/blast_radius_worker.go: Executes background jobs to compute repository blast radius scores asynchronously.
  • internal/blastradius/repocache/limits.go: Enforces disk limits and manages LRU cache eviction using active job locks and timestamps.

Impact

  • Functionality: Enables automated server-side PR diff scoring with disk space monitoring and administrative settings.
  • Risk: High disk usage from repository clones requires careful tuning of cache limits and volume space.

// UpdateBlastRadiusCacheSettings saves the switch and size limit; a lower limit evicts right away.
func (s *Server) UpdateBlastRadiusCacheSettings(c echo.Context) error {
// Shrinking the cache deletes cached repos for every org, so require a real session (CLAUDE.md session-only gating).
if authMethod, _ := c.Get(authMethodContextKey).(string); authMethod == authMethodAPIKey {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

The type assertion omits the 'ok' check, which means unauthenticated requests could bypass the API key block.

Suggestions:

  1. Check the comma-ok idiom on the type assertion: authMethod, ok := c.Get(authMethodContextKey).(string); !ok || authMethod == authMethodAPIKey

// removeOrphanIndexes deletes graph index files no cached repo points to (e.g. its repo
// was removed mid-job). ponytail: 1h age guard so a job still indexing keeps its files.
func removeOrphanIndexes(root string) {
used := map[string]bool{}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The map is being used uninitialized, which could lead to a potential read/write concurrency race.

Suggestions:

  1. Initialize used map or protect with mutex during concurrent entry processing

func indexFiles(root, entry string) []string {
project, err := os.ReadFile(filepath.Join(entry, ".index"))
name := strings.TrimSpace(string(project))
if err != nil || name == "" || strings.ContainsAny(name, `/\`) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The unhandled read error is being ignored, and the function incorrectly returns nil on failure.

Suggestions:

  1. Check err explicitly instead of relying on empty name check

}
dir := filepath.Join(base, "repo")

mu, _ := repoLocks.LoadOrStore(dir, &sync.Mutex{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

There is a potential race condition with LoadOrStore on a mutex pointer. The locked mutex can be returned to a concurrent caller.

Suggestions:

  1. Use a double-checked locking pattern or explicit locking around sync.Map
  2. Store pointers to structs containing a mutex and a reference count

if _, err = c.git(ctx, "checkout", "--force", "--detach", c.HeadSHA); err != nil {
return nil, nil, err
}
if err = os.WriteFile(filepath.Join(base, ".lastused"), nil, 0o644); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

There is a missing error check on WriteFile, which results in a silent failure when updating the lastused timestamp.

Suggestions:

  1. Check and return the error from os.WriteFile

@LiveReview-Bot

Copy link
Copy Markdown

Integrate Server-Side Blast Radius Scoring and Repository Caching

Overview

This change adds server-side blast radius calculation for webhooks and UI reviews using the git-lrc engine. It introduces a persistent, LRU-evicted local repository cache with size limits and integrates background job workers to process diff scores asynchronously.

Technical Highlights

  • internal/jobqueue/blast_radius_worker.go: Implements a background worker to clone repositories, check disk space, and compute blast radius scores.
  • internal/blastradius/repocache/limits.go: Enforces cache size limits using database-backed settings and LRU eviction logic.
  • storage/blastradius/store.go: Adds ReplaceFromReport to ingest and store complete blast radius report payloads atomically.
  • Dockerfile: Pins codebase-memory-mcp to v0.9.0 and adds git package dependencies for runtime execution.

Impact

  • Functionality: Enables automated blast radius reports for webhooks and UI-initiated reviews without requiring client-side CLI execution.
  • Risk: Requires at least 5 GB of free disk space on the lrdata volume for the persistent repository cache.


Let the server produce the same report the CLI produces, and store it through the path that already exists. Nothing downstream changes: blob storage, the Postgres mirror, Livi, and the diff viewer UI all keep working as-is.

The blast radius job runs **in parallel** with the AI review, the same way git-lrc does it locally — both start when the review is queued, neither waits for the other.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Parallel execution with AI review is beneficial as it avoids blocking the completion of the review process.

review page load -> UI joins comments + blast-radius report (already does)
```

**No server-side merge step is needed.** The two results already get combined at read time: the diff viewer fetches the blast-radius artifact separately and joins it to hunks by `file:new_start:new_lines`, and the finding-severity blend (`blendRiskScore`) happens there too. Whichever job finishes first just lands first; the page shows blast radius once the artifact exists.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Explicitly stating that no server-side merge is required clarifies the design and helps to avoid unnecessary complexity.


Cons: a small one-time change in git-lrc; LiveReview builds depend on git-lrc being reachable (or on the Go module proxy cache).

**Decision: Option B.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Option B is the correct choice because it prevents code drift and simplifies maintenance.

**Folder layout** (`./lrdata` is already the persistent volume in `docker-compose.yml`):

```
/app/lrdata/blastradius/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Good tenant isolation is achieved by using org_id and connector_id in the cache path.

| Flag | Effect |
|---|---|
| `--filter=blob:none` | Download commits + folder listings for history, but file contents only for the commit we check out |
| `--shallow-since="13 months ago"` | Drop history the engine never reads. 13, not 12, so the cut-off commit falls outside the engine's 1-year window (the cut-off commit looks like "every file added" and would fake co-change data) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Using a 13-month period for shallow clones is a good defensive measure against the 1-year engine window.

**Per job:**

```
lock <owner>__<repo>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Locking per repository prevents race conditions and ensures data integrity for the working tree.

- **Commit, not branch.** We always check out the exact `head_sha` the review is about, in "detached" mode. No `git pull`, no branch switching: a pull merges and can conflict, and a branch name moves while the job runs. Fetching by SHA works on GitHub/GitLab. For PRs from forks, the SHA isn't on any branch of the base repo — fetch the PR ref instead (`refs/pull/<n>/head` on GitHub, `refs/merge-requests/<n>/head` on GitLab).
- **Why reuse is fast:** the fetch only downloads commits and files that changed since the last cached commit, and the graph index updates incrementally instead of re-parsing the whole repo.
- **One job per repo at a time** (the `.lock`): there is one working tree, so two reviews of the same repo run one after the other. Different repos run in parallel.
- **Self-healing:** if any git step fails (corrupt clone, force-pushed history, etc.), delete `repo/` and clone once more before giving up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The self-healing mechanism, which involves deleting and re-cloning on failure, improves the system's robustness.

| Gitea | `https://<user>:<token>@<host>/owner/repo.git` |
| Azure DevOps | `https://pat:<token>@dev.azure.com/org/project/_git/repo` |

Pass the token through a credential helper / env, never in a logged URL or in `repo/.git/config`. SSH is not used: it would need a key registered per repo (GitHub deploy keys can't be shared across repos) or a machine-user account, plus `known_hosts` per git host, while every connector already has a token.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Using HTTPS with connector tokens is a good choice as it avoids the complexity of SSH key management.

Pass the token through a credential helper / env, never in a logged URL or in `repo/.git/config`. SSH is not used: it would need a key registered per repo (GitHub deploy keys can't be shared across repos) or a machine-user account, plus `known_hosts` per git host, while every connector already has a token.

2. **Graph index**
- The Docker image's `codebase-memory-mcp` is pinned to **v0.9.0** (same as git-lrc's `graphengine.PinnedVersion`) and locked in `PINNED_DOCKER_DEPS`. v0.10.x prints `query_graph` results as text even with `--json`, which the engine can't parse (checked with v0.10.8). Bump only together with git-lrc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Pinning the codebase-memory-mcp version is crucial for ensuring consistent scoring results.

3. **Scoring engine** — Option B above: `go get github.com/HexmosTech/git-lrc/blastradius@<commit>`, call `blastradius.ScoreHunks`.

4. **Job** — `internal/jobqueue/blast_radius_worker.go`
- `BlastRadiusJobArgs{OrgID, ReviewID}`, `Kind() = "blast_radius"`, on its own `blast_radius` River queue with 1 worker, so indexing never takes a review worker.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

A separate River queue and one worker for blast radius jobs prevent these jobs from starving the review workers.

- Repo and PR come from the review row, all queries scoped by `org_id`: `pr_mr_url` gives the clone URL and PR number for every provider; the connector (`integration_tokens`) gives the token, using the same PAT-vs-OAuth choice as `buildProviderConfig`; `pull_requests` (when linked) gives the base and source branch. The token is only ever sent to the connector's own host.
- Diff source: `git diff -M <merge-base> <head>` from the cached checkout (repocache), no provider API.
- Save via `blobstore.SaveArtifact` + `storageblastradius.Store.ReplaceFromReport` (moved out of `internal/api`; the CLI upload handler uses it too).
- Best-effort: 15 min timeout, `MaxAttempts: 1`, every failure logged and the job returns success, never affects the review.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The best-effort approach, with a 15-minute timeout and a maximum of one attempt, prevents blast radius job failures from blocking reviews.

- Diff source: `git diff -M <merge-base> <head>` from the cached checkout (repocache), no provider API.
- Save via `blobstore.SaveArtifact` + `storageblastradius.Store.ReplaceFromReport` (moved out of `internal/api`; the CLI upload handler uses it too).
- Best-effort: 15 min timeout, `MaxAttempts: 1`, every failure logged and the job returns success, never affects the review.
- **Repo too large for the cache** (phase 5, needs `max_gb`): 5 GB covers almost every repo without blobs. After the clone + index, if the repo alone is bigger than `max_gb`, delete it, write a `<owner>__<repo>.too_large` marker with the measured size, and save a small `blast-radius` artifact `{ "status": "skipped", "reason": "repo_too_large", "repo_gb": 7.2, "max_gb": 5 }` instead of a report. Later reviews of that repo skip straight away (no re-clone) until `max_gb` is raised above the recorded size. The skipped artifact ships together with the UI that renders it, since today's panel expects a full report.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Handling repo_too_large by skipping the artifact and providing UI feedback offers a good user experience.

- The section shows current cache usage (used / max GB, number of cached repos) so admins can see when to raise the limit.
- Same access as the rest of the Storage tab: `adminOrOwnerGroup` (`/settings/storage/...` in `server.go`) — self-hosted owners and super admins; super-admin only in cloud.
- Mega menu: add a `link('Blast Radius Repo Cache', ..., '/settings#storage', ...)` next to the existing Storage / Log Compaction / Preloaded Changes Archival entries in `megaMenuData.ts`, with the same predicate as `Storage`.
- **Install requirement**: document "at least 5 GB free disk on the `lrdata` volume" in the self-hosted install docs. If free space on the volume is too low for a clone, the job is skipped with the same notice as a too-large repo (step 4).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Documenting the disk space requirements for self-hosted installations is critical for successful deployments.

- **Install requirement**: document "at least 5 GB free disk on the `lrdata` volume" in the self-hosted install docs. If free space on the volume is too low for a clone, the job is skipped with the same notice as a too-large repo (step 4).

6. **Cache eviction (least-recently-used, size-based)** — at the end of every job, plus right away when an admin saves a lower cache size. No periodic job: the cache only grows when a job runs. No new tables.
- Size of a cached repo = its folder + its graph index files (`index/<project>.db*`, named in the repo's `.index` file). While the whole cache is over `max_gb`, delete the repo with the oldest `.lastused`, together with its index. Repos that are reviewed often keep getting touched, so they stay; rarely used repos are removed first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

LRU eviction based on size is an effective strategy for cache management.


6. **Cache eviction (least-recently-used, size-based)** — at the end of every job, plus right away when an admin saves a lower cache size. No periodic job: the cache only grows when a job runs. No new tables.
- Size of a cached repo = its folder + its graph index files (`index/<project>.db*`, named in the repo's `.index` file). While the whole cache is over `max_gb`, delete the repo with the oldest `.lastused`, together with its index. Repos that are reviewed often keep getting touched, so they stay; rarely used repos are removed first.
- Never evict a repo a job is using right now (its in-process lock is held).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Relying solely on job runs for eviction means the cache could temporarily exceed max_gb if jobs are infrequent. Consider a lightweight periodic sweep if this becomes an issue.

Suggestions:

  1. Add a lightweight periodic sweep if cache size becomes an issue during low job activity.

## Risks / open questions

- **Disk**: roughly one source tree + ~1 year of commit metadata + graph index per repo, capped by the admin-set `max_gb` (default 5 GB, min 1 GB, step 5). A repo bigger than `max_gb` is skipped and the review page says so, with a link to raise the limit (step 4).
- **Rename detection off**: with `diff.renames=false`, a renamed file shows up under both its old and new name in co-change data. Small effect; turn it back on if it matters (costs a one-time lazy download).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The diff.renames=false flag might slightly impact the accuracy of co-change detection for renamed files. Re-evaluate if this becomes a problem.

Suggestions:

  1. Monitor impact of diff.renames=false on co-change signal accuracy; re-enable if necessary.

- Internet connectivity for downloads
- Sufficient disk space for Docker images and data
- Sufficient disk space for Docker images and data, plus at least 5 GB free on the
`lrdata` volume for the blast radius repo cache (Settings → Storage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Adding the disk space requirement to the installer checklist is important for users installing the system in a self-hosted environment.

Comment thread go.mod
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/HexmosTech/git-lrc/blastradius v0.1.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

A new dependency, github.com/HexmosTech/git-lrc/blastradius, has been added, as per the design document.

// a cloned repo) are skipped explicitly, so nothing outside dir is ever counted.
func dirSize(dir string) int64 {
var size int64
_ = filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Errors encountered during WalkDir are ignored, and a partial size is returned upon failure.

Suggestions:

  1. Return or log the WalkDir error to avoid silent calculation failures.

const engineBinary = "codebase-memory-mcp"

// indexDir keeps graph indexes on the lrdata volume next to the repo cache, not in ~/.cache.
var indexDir = filepath.Join(repocache.DefaultRoot, "index")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

There is a compilation error because repocache.DefaultRoot is undefined.

Suggestions:

  1. Define DefaultRoot in repocache package or pass root parameter explicitly.


// repoLocks: <entry>/repo -> *sync.Mutex. Bounded by the cached repos: a key is deleted
// when its repo leaves the cache (removeIfIdle, MarkTooLarge).
var repoLocks sync.Map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The repoLocks sync.Map leaks mutexes due to missing cleanup logic.

Suggestions:

  1. Implement cache eviction and corresponding mutex deletion from repoLocks.

}
switch {
case strings.HasPrefix(provider, "gitea"):
var p struct{ Pat, Username string }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The patToken is unmarshaled as a gitea auth struct; consider using an explicit field or proper unmarshaling.

Suggestions:

  1. Define a dedicated struct for Gitea token JSON payload instead of reusing patToken string as JSON input

}
unified, _ := difflib.GetUnifiedDiffString(diff)
hunks := parseHunksFromUnifiedDiff(unified)
hunks := diffutil.ParseUnifiedHunks(unified)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Consider using the shared diffutil.ParseUnifiedHunks function.

}
var hunks []blastradius.HunkReport
for _, f := range report.Files {
hunks = append(hunks, f.Hunks...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

There is a potential for many small appends; consider pre-allocating slice capacity.

Suggestions:

  1. Calculate total hunks across all files and pre-allocate hunks slice capacity before the loop.

Comment thread ui/src/api/reviews.ts
* @returns Promise with the blast radius report
*/
export const getBlastRadiusReport = async (reviewId: number): Promise<BlastRadiusReport> => {
export const getBlastRadiusReport = async (reviewId: number): Promise<BlastRadiusReport | BlastRadiusSkipped> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The return type has been updated to include BlastRadiusSkipped.

Comment thread ui/src/api/reviews.ts
export const getBlastRadiusReport = async (reviewId: number): Promise<BlastRadiusReport | BlastRadiusSkipped> => {
try {
return await apiClient.get<BlastRadiusReport>(`/api/v1/diff-review/${reviewId}/artifacts/blast-radius`);
return await apiClient.get<BlastRadiusReport | BlastRadiusSkipped>(`/api/v1/diff-review/${reviewId}/artifacts/blast-radius`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The API client type parameter has been updated to accommodate BlastRadiusSkipped.

@LinceMathew
LinceMathew merged commit 97889ba into master Oct 10, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants