Code-Reducer is an interactive LLM-driven static analysis pipeline that generates hierarchical markdown documentation for software repositories. The binary entry point (main.go) delegates execution to cmd.RootCmd.Execute(), which routes through a lifecycle of three engine modes—init, update, and one additional mode resolved by executeCommand. All domain logic lives in the internal/ packages; no domain logic is implemented at the top level.
User Terminal ──► RootCmd (cobra) ◄──► executeCommand(Mode)
├──► checkAndRunSetup(repoRoot)
│ Check git repo, config existence, TTY check
│ If no config + stdin is TTY → RunSetupFlow
├──► checkInitStatus(repoRoot, docsDir, Mode)
│ Validates init marker presence; enforces:
│ - ModeInit fails if already initialized
│ - ModeUpdate fails if not yet initialized
├──► runEngine(repoRoot, cfg, Mode)
│ Registers SIGINT/SIGTERM handlers
│ Instantiates runner
│ Streams events (status → stdout, error → stderr)
└──► executeCommand returns engine.Error or nil
The static analysis pipeline itself follows: repository discovery (internal/tools) → path safety validation (internal/security) → configuration resolution (internal/config) → orchestration and synthesis (internal/engine). All external dependencies funnel through the llmCaller interface defined in internal/config, enabling transport-level abstraction without coupling to a specific LLM provider.
| Package | Role |
|---|---|
internal/tools |
Safe file I/O with TOCTOU protection and atomic writes; git command abstraction for subprocess execution |
internal/security |
Path traversal prevention via SafeResolve; cross-process lock management via SimpleLock/AcquireLock/Unlock |
internal/config |
Runtime configuration schema (LLM prompts, extraction steps, ignore lists); atomic config persistence; multi-source resolution (YAML → env → flags → defaults) |
internal/engine |
End-to-end pipeline: discovers source files, classifies change status against cached state, recursively synthesizes hierarchical summaries via LLM calls, persists results as markdown documentation |
Config is the single struct passed between SaveConfig, LoadConfig, and ResolveConfig. All three functions operate on it by value or pointer — SaveConfig and LoadConfig accept only a directory path, while ResolveConfig takes directory plus two flag arguments. No methods are defined on Config; all mutation happens in external packages via struct initialization.
- Hardcoded defaults
- YAML config file (
.code-reducer.yaml) - Environment variables (
CODE_REDUCER_MODEL_ID,OLLAMA_BASE_URL,OLLAMA_NUM_CTX) - CLI flags
If the YAML file does not exist, absence is accepted as valid and an empty Config{} is substituted. Parse errors are wrapped with "failed to parse yaml config:"; I/O faults become "failed to load configuration file:".
DefaultExtractionSteps is a package-level var of type []ExtractionStep, pre-populated with four entries:
| Index | Name | Purpose |
|---|---|---|
| 0 | API_SIGNATURES |
Extracts public types, functions, methods and their signatures without explaining internal logic. |
| 1 | BUSINESS_LOGIC |
Explains the primary domain problem solved by the code and lists high-level algorithm steps, ignoring implementation syntax. |
| 2 | STATE_AND_CONCURRENCY |
Identifies mutable global/state variables and synchronization mechanisms; outputs "No mutable state" if entirely stateless. |
| 3 | ERRORS_AND_SIDE_EFFECTS |
Details interactions with external systems (network, disk, databases) and how errors propagate or are swallowed. |
The engine orchestrates documentation generation in two modes:
- Init (
ModeInit) — full regeneration of all documentation - Update (
mode ModeUpdate) — incremental reprocessing limited to directories whose descendants contain changed files
Runner.Run()→ orchestrator selection → tree construction → per-node synthesis (bottom-up) → global doc generation → cache persistence- All external dependencies funnel through the
llmCallerinterface, enabling transport-level abstraction without coupling the engine layer to a specific LLM provider.
Interaction is abstracted behind the llmCaller interface so the engine layer can depend on it without tying to a specific transport implementation. Interaction targets Ollama's api/chat protocol via HTTP POST:
- Prepend any system prompt to the user-provided message list
- Serialize into Ollama's expected JSON schema (model ID, messages, stream flag, optional format/options)
- Execute synchronous HTTP POST to
baseURL/api/chatwith a context-aware client and fixed timeout; no retries are attempted - On 200 OK, deserialize the JSON into an Ollama-style envelope and return only the model's reply content
Error Propagation: Non-OK status returns "ollama api error: status {code}, response: {text}" with body read up to maxErrorBodyBytes. Successful status plus parse error yields a wrapped error "failed to parse response: %w". Transport failure (network down, DNS fail, timeout) is returned directly without retry logic.
The engine persists per-file extraction results ("facts") along with file integrity hashes, enabling change detection and incremental reprocessing between pipeline runs. The cache is stored as a versioned JSON file at <docsDir>/<metadataFileName> relative to repoRoot.
Initialization and Loading: Initialize empty cache → load existing cache from disk (missing file returns fresh empty cache; incompatible versions silently recover) → nil safety: maps initialized to nil on load are replaced with fresh empty maps.
This code builds an in-memory directory tree from file paths, then determines which directories are affected by a set of file changes (additions, modifications, deletions) by traversing the tree and marking parent directories whose children have changed or been removed.
Algorithm Steps:
- Parse each input file path into hierarchical components (
/-separated), recursively constructingDirNodeobjects that represent folders with their files and child subdirectories - Initialize affected set; for each file change, if its status is "Deleted", immediately mark its parent directory as affected
- Walk every
DirNode: if any file under that node appears in a changed-file map, mark the node's path as affected; check whether a corresponding markdown module path exists at disk (relative todocs/modules/). If it does not exist, mark the directory as affected — implying the module is being added or removed. Recursively process all child directories - Walk again: if any descendant is marked affected, propagate that status upward to parent nodes so ancestors of changed subtrees are also flagged
This code implements an automated codebase summarization engine that recursively analyzes source files and directories, extracting structured facts about each file's purpose/behavior via LLM calls, then synthesizing hierarchical summaries upward through a directory tree — ultimately producing per-directory documentation artifacts.
Algorithm Steps:
- Tree Traversal (Bottom-Up): Recursively process a directory node by first visiting all child directories (sorted for determinism), then processing files within the current directory
- File Fact Extraction: check cache and precomputed hashes before reading file content; read raw file bytes, compute SHA-256 hash if not already cached; split file content into overlapping chunks (size dynamically scaled from LLM context window); for each chunk, invoke an LLM with a system prompt + step-specific user prompt to extract facts; consolidate all chunk results through a reduction step per extraction step. Repeat across multiple sequential extraction steps until all perspectives are captured
- Component Assembly: Combine extracted file summaries and synthesized child-directory summaries into a unified list of components for the current directory node
- Directory Synthesis: Apply multi-step LLM-based chunked reduction on the assembled components to produce a single consolidated summary for the entire directory
- Persistence and Caching: Store computed hashes, file facts, and directory summaries in metadata caches; write final directory summaries to disk as markdown documentation under
cfg.DocsDir/modules/<safe-filename>
This module implements a map-reduce tree reduction algorithm for LLM-based code synthesis. It takes multiple text items (code facts, file descriptions, architecture notes), batches them within context-window limits, sends each batch to an LLM, and recursively reduces the outputs until a single consolidated result remains — stopping when further reduction would not shrink the output significantly (loop prevention).
Algorithm Steps:
- Return empty string if no items provided; return item as-is for single-item inputs in
reduceFileFacts - Pre-Expansion: Any individual item exceeding the character limit is split into smaller overlapping chunks via
chunkTextWithOverlap, then all resulting pieces are pooled back into a flat list - Binning by Size: Items (and pre-expanded chunks) are grouped into batches such that no batch exceeds
maxCharsrunes, with overflow items starting a new batch - Recursive Reduction: Each batch is sent to the LLM via
reduceFn. The function calls itself recursively on each batch's result until only one item remains inintermediate - Loop Prevention Check: Before recursing again, total output runes are compared against 95% of total input runes. If output ≥ 95% of input (information is not being condensed), the algorithm stops and returns all intermediate results concatenated with double newlines, preserving information without exceeding context windows
- LLM Integration: The LLM caller receives a system prompt plus user content formed by joining batch items with double newlines. Markdown fences are stripped from the response before return via
stripOuterMarkdownFence
This code implements a Map-Reduce pipeline that automatically generates and maintains documentation for software repositories by recursively analyzing source code structure with an LLM, then writing the synthesized results back as markdown files (architecture overview, quickstart guide, per-module summaries).
Algorithm Steps:
- Code Discovery: Locate all code files in the repository root, filtering out documentation directories and patterns from
.gitignore+ user-configured ignore lists - Hash-Based Change Detection: Compute SHA256 hashes for each discovered file and compare against a cached state to classify changes as Added, Modified, or Deleted
- Tree Construction: Organize the code files into a hierarchical directory tree structure (
DirNodewith children) - Affected Directory Determination: Init mode: Mark all directories as affected (full regeneration). Update mode: Identify only those directories whose descendants contain changed files, propagating "affected" status upward through the tree
- Hierarchical Tree-Merging: Recursively synthesize each node starting from leaves and working toward the root. Each synthesis call to the LLM produces a summary of that directory's code, which becomes input for parent-level synthesis (reducing as you move up)
- Standard Documentation Generation: After the tree is fully synthesized, generate two global documents:
architecture.md— high-level system overview based on the root synthesis output;quickstart.md— onboarding and usage guide derived from the same root summary - Agent Guidelines Update: Write or append an AI Agent Guidelines file that references all generated documentation paths, ensuring future agent interactions are informed by existing docs
- Cache Maintenance: Pruning: Remove cache entries for directories no longer present in the code tree and delete their corresponding markdown files on disk. Invalidation: If extraction steps configuration changes, reset the file cache to force full regeneration on next run
This code manages and orchestrates an automated documentation generation pipeline for software projects, supporting both initial documentation creation (init mode) and incremental updates to existing documentation (update mode). It serves as the main entry point that coordinates AI-powered document processing across a repository.
Algorithm Steps:
- Ensure project lockfile is added to
.gitignorefor version control safety - Acquire an exclusive repository lock to prevent concurrent documentation operations
- Initialize LLM client and orchestrator based on provided configuration (model, base URL, context settings)
- Execute the appropriate pipeline mode: either run full initialization or incremental update using the configured AI model
Synchronization and Locking: A repository-level lock is acquired via security.AcquireLock(repoRoot) and released via defer lock.Unlock(). This protects repository state during pipeline execution. The exact lock type (file-based, in-memory) depends on implementation in internal/security. All other struct fields are assigned once in NewRunner() and never modified after that; no mutable shared state exists within this file's boundary.
This file provides lightweight utility helpers for the engine module: generating safe markdown filenames from directory paths, and creating adapter-style log event callbacks that support optional listeners.
Domain Rules:
- Empty/
.module paths map to"README.md"by default viatoSafeMarkdownFilename - Unknown or invalid callbacks are silently swallowed — no panic on nil listeners in
makeLogEvent
This module provides two security primitives scoped to a repository root:
- Path traversal prevention via
SafeResolve, which validates that user-supplied paths resolve within the repository boundary, resolving symlinks on existing ancestor parts before comparison - Cross-process lock management via
SimpleLock/AcquireLock/Unlock, which ensures only one process holds exclusive access to a protected resource at any given time through an atomic file-based lock mechanism
Both primitives operate against the local filesystem; no network, database, or remote API interactions occur within production code. All errors are returned to callers and propagated via Go's error interface; panics are not raised by this module.
func SafeResolve(repoRoot, inputPath string) (string, error)Resolves a candidate path against an anchor root while preventing escape through symlinks and directory traversal. The function returns the cleaned absolute path if it remains strictly inside the resolved repository boundary; otherwise it returns ErrPathTraversal.
Algorithm Steps:
- Compute the absolute root directory from
repoRootviafilepath.Abs, wrapping any resulting error with%w - Resolve symlinks on the absolute root using
filepath.EvalSymlinks(absRoot), again wrapping errors with%w - Walk up from the joined path (
absRoot + inputPath) until a physically existing ancestor is found via repeatedos.Lstat(current)calls; each Lstat failure that is not "not exist" is wrapped and returned immediately - Resolve symlinks on the first physically existing ancestor via
filepath.EvalSymlinks(current), wrapping errors with%w - Reconstruct the full target path from the resolved ancestor plus all previously-skipped components
- Verify that the reconstructed path remains inside the resolved root; reject if it escapes by returning a wrapped error using
ErrPathTraversalwithinputPathas context
Algorithm Steps:
- Calls
SafeResolve(repoRoot, LockFileName)to obtain the canonical lock file path inside the repo root. If this fails, the error propagates directly without wrapping - Opens the lockfile with
os.OpenFile(lockPath, O_WRONLY|O_CREATE|O_EXCL, 0644). The OS-level atomicity of O_EXCL means failure indicates another writer holds the file or a stale lock persists; this is treated as an error condition requiring manual cleanup by the caller - Writes the current process PID into the lockfile via
f.Write([]byte(fmt.Sprintf("%d\n", os.Getpid())))for identification/inspection purposes. If this write fails, the method closes the file and removes it before returning a wrapped error
*Unlock() error (method on SimpleLock) — Releases the lock by closing the file descriptor and removing the lockfile from disk. Idempotent and thread-safe with respect to itself. The struct owns its own mutex; concurrent calls to Unlock on the same instance serialize through mu.Lock()/defer mu.Unlock(), making the operation atomic with respect to itself.
The internal/tools package provides two complementary capabilities for analyzing a code repository's structure: (1) safe file I/O operations with TOCTOU protection and atomic writes, and (2) Git command abstraction for process execution. All functions operate against a local filesystem only; no network calls, database interactions, or external API invocations occur.
func WriteFileAtomic(targetPath string, data []byte, perm os.FileMode) errorWrites binary data to targetPath using a temp-file + rename pattern that prevents partial or corrupt state if interrupted. The directory is created with mode 0755. Returns an error on any failure; returns nil only after the final rename succeeds.
Sequence:
- Create temporary file in same directory as target (ensures atomic rename semantics)
- Write data to temp file, sync before close for crash safety
- Close and flush via defer block with cleanup gated by success flag
- Apply
permmode viaos.Chmod - Atomic rename from temp path into final location
Errors at any step are wrapped; deferred cleanup runs on failure paths to prevent partial temp file leakage. No panic/recover anywhere in this function.
func ReadFileSafely(repoRoot string, virtualPath string) ([]byte, error)Resolves the virtual path against repoRoot, then reads via os.ReadFile. Wraps any read failure with context ("failed to read file content"). The path resolution error from security.SafeResolve propagates unwrapped.
func WriteFileSafely(repoRoot string, virtualPath string, data []byte) errorResolves the virtual path against repoRoot, then writes via os.WriteFile. Wraps any write failure with context ("failed to write file content"). The path resolution error from security.SafeResolve propagates unwrapped.
func RunGit(repoRoot string, args ...string) (string, error)Executes a git subprocess via exec.CommandContext. Returns stdout on success; wraps any failure with context ("failed to run git"). The command is spawned in the repository root so relative paths work naturally.