Raw capture โ€” raw/local-review.md


url: https://github.com/rosenbjerg/local-review title: local-review author: rosenbjerg (Malthe Rosenbjerg) date_fetched: 2026-07-11 date_published: 2026-06


local-review

A local, single-user git review tool: review a branch's diff, leave line/range comments, mark files reviewed, and export the review as markdown for a coding agent. Go backend + React frontend, shipped as one binary (the built frontend is embedded via go:embed).

Architecture

Stack

Server (main.go, 143 lines)

Git service (internal/git/git.go, 451 lines)

Store (internal/store/store.go, 629 lines)

API handlers (internal/api/api.go, 751 lines)

SSE events (internal/api/events.go, 57 lines)

Comment anchoring (internal/api/annotate.go, 192 lines)

Reviewed file tracking (internal/api/reviewed.go, 63 lines)

Markdown export (internal/export/export.go, 219 lines)

Frontend (web/src/)

Build

Key Techniques

Drift-resistant anchoring

The project's core innovation: comments survive branch rebasing and new commits. Uses a two-tier system: (1) precise git line tracking via git diff <commit_sha> head -- path with MapOldLine hunks โ€” catches genuine moves vs coincidental reappearances; (2) snippet matching as fallback for worktree comments, comments without commit_sha, and binary/renamed files. Each comment records the commit_sha it was anchored against (resolved at add time) and a worktree flag for which side to compare against.

Derived state, never persisted

Comment staleness (anchorStatus, currentStartLine, currentEndLine) and reviewed-file validity are recomputed on every read, never trusted from stored flags. The fields exist on the Comment struct with omitempty JSON tags but the store never reads or writes them โ€” they're filled by annotateReview() in the API layer. This means the reviewer always sees current truth even after force-pushes or rebases.

Reviewed-files with content fingerprinting

When a file is marked reviewed, a SHA-256 fingerprint of its current content is captured alongside a worktree flag. On every review read, annotateReview() re-hashes the current content and drops any file whose fingerprint no longer matches. Changed files automatically revert to unread โ€” no stale "reviewed" checkmarks after new commits.

Coalescing SSE hub

The in-memory pub/sub hub uses size-1 buffered channels with non-blocking sends. A dropped ping is harmless: every ping makes the client refetch full canonical state, so at most one refresh is ever owed. Empty review entries are pruned on last unsubscribe. 25s keepalive comments keep streams warm and turn half-open connections into write errors.

Backtick-safe markdown export

fenceFor() scans the snippet for the longest run of backticks, returns a fence one longer (minimum 3). Prevents a snippet containing triple-backtick code fences from prematurely closing the markdown code block โ€” a real edge case when reviewing markdown files that contain code examples.

Idempotent schema migration

ensureColumn(table, column, ddl) checks PRAGMA table_info for existing columns before ALTER TABLE ADD COLUMN. Each is a no-op once present, making schema additions safe across upgrades without a migration framework. Used for 7 columns added after the initial schema.

Working tree diff with untracked files

DiffWorktree goes beyond git diff <base> to include untracked non-ignored files via git ls-files --others --exclude-standard -z (NUL-separated for safe path handling). Each untracked file gets a synthesized FileDiff with newFileDiff() โ€” binary detection via NUL byte, empty files get no hunks, text files get a single "all added" hunk.

Custom diff parser with content-safety

The parseDiff function correctly handles edge cases that trip up naive parsers: ---/+++ appearing in content (deleted SQL comments, added ++ lines) by checking hunk context before header matching. Also handles \ No newline at end of file escape lines, binary file detection, rename chains, and forces canonical a//b/ path prefixes regardless of user git config.

Review resume regardless of export status

CreateOrGetReview matches on (repo_path, base_ref, head_ref) regardless of status โ€” so exporting (which sets status to exported) never orphans an in-progress review. Re-opening the same branch resumes it with all comments intact.

Design Decisions

Git subprocess vs go-git library

Chosen: shell out to git binary. Correct on renames, submodules, .gitattributes diff filters, merge strategies. The trade-off is git must be installed (no pure-Go portability) and subprocess startup overhead. The right call for a tool whose entire purpose is reading git state โ€” correctness on real repos beats theoretical portability.

Single connection SQLite vs connection pool

Chosen: SetMaxOpenConns(1). SQLite's foreign_keys pragma is per-connection โ€” with a pooled DB some connections would miss it and skip ON DELETE CASCADE. Single connection makes the pragma authoritative and gives transactions full check-then-insert atomicity. DB access is low-frequency (git diffs/file reads don't hit SQLite), so serialization is free. A bold but correct call.

Backend as source of truth vs client-owned state

Chosen: backend is canonical, frontend caches. Discrete actions (add/delete/toggle) save immediately. SSE hub broadcasts "changed" pings; clients refetch full state on ping. This means the backend can derive staleness, filter reviewed files, and compute anchor status on every read โ€” the frontend never has to maintain its own truth. No conflict resolution, no optimistic update bugs beyond transient flickers.

Two-level threads vs arbitrary nesting

Chosen: comment (root) + replies (flat). Comments have type, anchor, snippet, resolve; replies have only body + timestamp. FK cascades ensure replies never orphan. A conscious simplification โ€” GitHub-style deep threading is complex to render and navigate. Two levels is enough for "reviewer says X, agent replies Y, reviewer acknowledges."

Derived staleness vs write-time persistence

Chosen: recompute on every read. The alternative โ€” update all comments on every push โ€” is expensive and easily wrong. Recomputing on read means a freshly pulled branch always shows correct anchor status. The cost is O(comments ร— files/diffs) per review load, but reviews are human-scale (tens to hundreds of comments), not machine-scale.

Ping-and-refetch SSE vs per-event payloads

Chosen: simple data: changed pings only. Clients refetch the full review. This means the SSE hub has no payload to serialize or version, no risk of desync between event types, and a dropped ping is at most one missed refresh. The alternative (sending full comment/reply state in SSE events) would mean duplicating serialization logic and keeping event payloads in sync with the REST API.

JS regex engine for Shiki vs oniguruma WASM

Chosen: JS regex with forgiving: true. Avoids the browser WASM load failure that Shiki's default oniguruma engine triggers in some environments. forgiving skips the few oniguruma-only grammar patterns instead of throwing. The trade-off: some complex patterns (backreferences, lookbehind in certain grammars) degrade gracefully to unhighlighted tokens rather than breaking the whole page.

Responsiveness patterns for large diffs

Chosen: multiple defense layers. LazyFile IntersectionObserver viewport-mounting avoids fetching/tokenizing off-screen files. Files > 500 changed lines auto-collapse. Files > 2000 lines skip syntax highlighting. Panel resize writes grid-template-columns directly to the DOM during drag (no React re-render per mousemove). A practical stack โ€” not a single silver bullet, but layers that each cut a different cost.

No authentication or multi-user support

Chosen: local-only, single-user. This is the defining constraint that makes the architecture simple. No auth middleware, no user table, no permissions. The server listens on 127.0.0.1 only. The trade-off is no team collaboration, but the purpose is "review before handing to agent," not "team code review."

Comparison Notes

Unlike [[OpenCodeReview]] (Alibaba's AI code review CLI), which uses AI to generate review comments, local-review is a tool for humans to write review comments that are then handed to an AI coding agent. The direction is inverted: OpenCodeReview has AI reviewing human code; local-review has humans reviewing code for AI to act on.

Unlike [[Bram]], which layers a two-gate approval workflow over Claude Code/Codex CLI sessions, local-review is purely a review artifact generator โ€” it doesn't execute or approve anything. It produces markdown, which can feed into Bram, Claude Code, or any other agent.

Unlike [[DeltaDB]] (Zed's version control for the agent era), which traces every line of code to the conversation that produced it, local-review only cares about the review moment โ€” a snapshot of what the human thought about the code at a particular point in time.

Unlike [[Metis โ€” ARM AI Security Code Review]], which uses tree-sitter for deterministic call-graph analysis, local-review makes no attempt to understand code semantics. It operates purely at the diff and line level, leaving all understanding to the human reviewer and the receiving coding agent.

The tech stack echoes [[Memento]] (Go + SQLite + SSE runtime), but for a different purpose: Memento is a knowledge layer over email; local-review is a review layer over git.


Source: https://github.com/rosenbjerg/local-review Analyzed: 2026-07-11 Codebase: ~1,100 lines Go (main.go + internal/), ~1,100 lines TypeScript (web/src/), ~200 lines CSS, ~300 lines tests Dependencies: Go stdlib + modernc.org/sqlite; React 19 + Shiki + markdown-it + Vite