SF-Assistant (Zero-Dependency RAG Tool)
Role: Solo, end-to-end
Tech Stack: Java 11 (Stdlib Only), OpenAI/Anthropic APIs, StAX, com.sun.net.httpserver
Repository: Private. Demo available on request
Overview
SF-Assistant is a conversational codebase assistant for Salesforce projects, built under an unusual constraint: zero external dependencies. No Maven. No Gradle. No Jackson. Just Java 11 standard library.
That constraint is the whole point. I kept inheriting large Salesforce codebases with no documentation, working in environments where installing dependencies on the orgβs machines wasnβt an option. Most RAG tooling assumes you can pip install your way to success. In the enterprise environments I actually deal with, you canβt.
The tool combines RAG (Retrieval-Augmented Generation) with call graph analysis to help developers understand legacy codebases through natural language. It provides:
- Semantic Code Search, find code by describing what it does, not just matching keywords
- Debug Log Analysis, parse Salesforce debug logs to identify performance bottlenecks and exceptions
- Discovery Commands, systematically explore unfamiliar codebases with
/search,/list,/graph,/stats - Dependency Analysis, trace execution paths, find callers, visualize dependency trees
- Lean Format Transformation, automatically reduce verbose XML metadata by 70-90% for better embeddings
- LLM Autonomous Research, the LLM can invoke tools like
/graphand/depsto gather context before answering
It ships with both a terminal REPL and a web interface served by the embedded zero-dependency server, which streams per-command progress to the browser over SSE. Output renders per medium: an ANSI markdown renderer for the terminal, HTML for the web. All of it keeps memory footprint minimal through lazy loading.
System Architecture
The system runs a hybrid retrieval strategy, combining semantic search with structural code analysis for accurate, grounded responses.
sequenceDiagram
participant User
participant Repl
participant EmbeddingService
participant OpenAI_API
participant IndexStore
participant ContextBuilder
participant LlmClient
participant LLM_Provider_API
User->>Repl: "How does AccountHandler work?"
rect rgba(100, 180, 220, 0.2)
note right of Repl: 1. Vectorize Query
Repl->>EmbeddingService: getQueryEmbedding("...")
EmbeddingService->>EmbeddingService: Check Cache
alt Cache Miss
EmbeddingService->>OpenAI_API: POST /embeddings
OpenAI_API-->>EmbeddingService: Vector[1536]
end
EmbeddingService-->>Repl: float[] queryVector
end
rect rgba(140, 120, 200, 0.2)
note right of Repl: 2. Retrieval & Context
Repl->>ContextBuilder: buildPromptWithDependencies(...)
ContextBuilder->>IndexStore: findRelevant(queryVector)
IndexStore-->>ContextBuilder: List of ScoredUnit Seeds
ContextBuilder->>ContextBuilder: DependencyExpander.expand()
note right of ContextBuilder: BFS Traversal of Call Graph
ContextBuilder->>ContextBuilder: Apply Token Budget
note right of ContextBuilder: 1. System Prompt<br/>2. Pinned Files<br/>3. Retrieved Chunks<br/>4. Chat History
ContextBuilder-->>Repl: Prompt Object
end
rect rgba(220, 140, 140, 0.2)
note right of Repl: 3. LLM Reasoning
Repl->>LlmClient: chat(Prompt)
LlmClient->>LLM_Provider_API: POST /chat/completions
LLM_Provider_API-->>LlmClient: "The AccountHandler class..."
end
LlmClient-->>Repl: Response String
Repl-->>User: Display Answer
Whatβs Inside
- Lazy-Loaded Indexer: stores file offsets instead of content, reading from disk only when needed for LLM context.
- Hybrid Chunker: splits large files into CLASS_HEADER, METHOD, and TEXT_CHUNK units based on size thresholds.
- Call Graph Expander: BFS traversal that automatically includes related methods (callers/callees) beyond semantic search results.
- Provider Abstraction: unified interface for OpenAI and Anthropic APIs with automatic request/response format handling.
- Debug Log Analyzer: parses profiling data and stack traces, automatically retrieving code at failure points.
- Lean Format Transformer: converts verbose Salesforce XML to token-efficient format during indexing.
Debug Log Analysis
This is what turns SF-Assistant from a code Q&A tool into an actual debugging assistant.
The Challenge
Salesforce debug logs are massive (often 500k+ tokens) and nightmarish to analyze:
- 90%+ of the content is noise (system code, timestamps, internal operations)
- Critical data (governor limits, execution patterns) is buried in verbose output
- Manual analysis means finding class files, understanding context, piecing together execution flow
The Solution
SF-Assistant implements a Parse β Cross-Reference β Correlate pipeline:
/profiling debug.log
PROFILING ANALYSIS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
GOVERNOR LIMITS
SOQL Queries: 94/100 (94%)
CPU Time: 8,234/10,000ms (82%)
METHOD EXECUTION
AccountService.processUpdates:67 47 calls 7,892ms total
AccountTriggerHandler.onAfterUpdate 47 calls 234ms total
SOQL EXECUTION
SELECT Id, Amount FROM Opportunity WHERE AccountId = :acc.Id
47 executions 892ms total
CORRELATIONS
β’ AccountService.processUpdates and SOQL query both executed 47 times
β’ processUpdates consumed 96% of total CPU time
RETRIEVED CODE (auto-pinned)
β AccountService.cls
β AccountTriggerHandler.cls
Key Design Principle
βStructure data, retrieve code, present facts. Let the LLM diagnose.β
The analyzer identifies correlations (βmethod and query both executed 47 timesβ) but deliberately avoids pre-labeling them as anti-patterns. The same pattern could be:
- Legitimate trigger recursion (bulkified code)
- Intentional batch processing
- An actual SOQL-in-loop problem
The LLM examines the actual code alongside the metrics to determine root cause, which is something algorithmic pattern-matching cannot reliably do.
Token Savings: debug log analysis achieves 80-95% token reduction from raw logs while preserving all diagnostic information.
Discovery & Navigation
Commands for systematic codebase exploration, added in v0.5.0.
/list [type]: Codebase Inventory
> /list
Codebase Inventory:
Type Count Avg Tokens Total Tokens
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CLASS 89 342 30,438
METHOD 654 127 83,058
TRIGGER 12 456 5,472
FLOW 23 234 5,382
VALIDATION_RULE 45 156 7,020
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
TOTAL 1,080 181 195,260
/search <query>: Semantic Code Search
> /search SOQL queries with limits
1. AccountService.cls:method:getAccounts (similarity: 0.87)
Preview: public List<Account> getAccounts() { return [SELECT Id FROM Account LIMIT 100]; }
2. ContactHelper.cls:method:fetchContacts (similarity: 0.82)
Preview: Database.query('SELECT Id FROM Contact WHERE Email = \'' + email + '\' LIMIT 50');
/graph <classname>: Call Relationships
> /graph AccountService
OUTGOING CALLS (What this class calls):
ββ ContactService.updateContacts()
ββ AccountValidator.validate()
ββ Database.insert() [System]
INCOMING CALLS (What calls this class):
ββ AccountTriggerHandler.onUpdate()
ββ AccountController.saveAccount()
ββ AccountBatch.execute()
These tools let developers understand codebase structure before asking targeted questions, which cuts down on βI donβt have that informationβ responses significantly.
Dependency Analysis
Tools for understanding code relationships and execution paths.
/deps <class> [depth]: Dependency Tree
Visualize multi-level dependency chains showing what a class calls and what those call recursively.
> /deps AccountService 2
Dependency Tree for AccountService (depth: 2)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AccountService
ββ ContactService.updateContacts()
β ββ Database.update() [System]
β ββ ContactValidator.validate()
ββ AccountValidator.validate()
β ββ ValidationUtils.checkRequired()
ββ Database.insert() [System]
Statistics:
Total Units: 5
Total Dependencies: 6
Max Depth: 2
/callers <method>: Reverse Call Lookup
Find all code that calls a specific method. Essential for impact analysis before refactoring.
> /callers AccountService.validate
Callers of AccountService.validate
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Found 4 callers:
1. AccountTriggerHandler.onBeforeInsert()
2. AccountTriggerHandler.onBeforeUpdate()
3. AccountController.saveAccount()
4. AccountBatchJob.execute()
Impact Assessment: MEDIUM (4 callers)
/flow [flowname]: Flow Definition Viewer
Display Salesforce flow definitions in lean format, or list all flows in the codebase.
> /flow Account_Validation_Flow
Flow: Account_Validation_Flow
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Type: Record-Triggered Flow
Status: Active
Object: Account
Trigger: BeforeInsert, BeforeUpdate
Elements:
ββ Get_Related_Contacts (recordLookup)
ββ Check_Required_Fields (decision)
β ββ True β Set_Error_Message
β ββ False β Update_Status
ββ Final_Validation (decision)
Codebase Analysis
/similar <filename>: Find Similar Code
Discover semantically similar code units for pattern detection and deduplication opportunities.
> /similar AccountTriggerHandler
Similar Code Units to AccountTriggerHandler
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. ContactTriggerHandler.cls:class (similarity: 0.89)
2. OpportunityTriggerHandler.cls:class (similarity: 0.85)
3. LeadTriggerHandler.cls:class (similarity: 0.78)
Analysis: This appears to be a trigger handler class.
Consider following consistent patterns across handlers.
/stats: Comprehensive Statistics
> /stats
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SF-ASSISTANT CODEBASE STATISTICS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
INDEX INFORMATION
Total Code Units: 1,247
Total Tokens: 342,567
Average Unit Size: 274 tokens
CODE DISTRIBUTION
Apex Classes: 89 (45,678 tokens)
Apex Methods: 654 (83,058 tokens)
Apex Triggers: 12 (5,472 tokens)
CALL GRAPH
Total Edges: 1,892
Most Called: ValidationUtils.checkRequired() (67 refs)
Most Active: AccountService.processUpdates() (23 calls)
Lean Format Transformation
SF-Assistant automatically transforms verbose Salesforce XML metadata into token-efficient βlean formatβ during indexing.
Token Savings
| Metadata Type | Token Reduction | Example |
|---|---|---|
| Flows | 70-90% | 13,130 tokens β 2,534 tokens |
| Validation Rules | 35-50% | 155 tokens β 94 tokens |
| Formula Fields | 60-70% | Varies by complexity |
| Custom Metadata | 60-70% | Varies by field count |
Example Transformation
Before (XML):
<decisions>
<processMetadataValues>
<name>index</name>
<value><numberValue>0.0</numberValue></value>
</processMetadataValues>
<name>Decision_Amount_Check</name>
<locationX>182</locationX>
<locationY>350</locationY>
<defaultConnector><targetReference>Screen_Rejected</targetReference></defaultConnector>
<rules>
<name>High_Value</name>
<conditions>
<leftValueReference>Amount</leftValueReference>
<operator>GreaterThan</operator>
<rightValue><numberValue>10000.0</numberValue></rightValue>
</conditions>
<connector><targetReference>Action_Notify_Manager</targetReference></connector>
</rules>
</decisions>
After (Lean Format):
=== DECISIONS ===
Decision: Decision_Amount_Check (Amount Check)
Default β Screen_Rejected
Rule: High_Value (High Value)
If: Amount > 10000.0
Then β Action_Notify_Manager
Benefits: better embeddings (more semantic meaning per token), improved search relevance (less XML noise), lower API costs (fewer tokens to embed and retrieve).
Engineering Details
1. Zero External Dependencies
The tool has to run in restricted enterprise environments where external libraries are flat-out prohibited. So I implemented all the infrastructure from scratch using only Java 11 stdlib.
- Custom JSON Parser: recursive descent parser (
SimpleJsonParser) handles API serialization/deserialization without Jackson/Gson. - Custom HTTP Client: built on
java.net.HttpURLConnectionwith provider-specific authentication and request formats. - Custom XML Parser: uses StAX (
javax.xml.stream) for Salesforce metadata extraction with XXE protection. - Embedded Web Server: built on
com.sun.net.httpserverfor the browser-based terminal UI with zero dependencies.
Single JAR deployment. No build tools required, just javac and jar.
2. Memory-Efficient Lazy Loading
Traditional RAG systems load entire codebases into memory, which causes OOM errors on large projects. So I built a lazy-loading architecture where CodeUnit objects store file offsets instead of content.
- Content reads from disk only when needed (during LLM context assembly).
- A redactβstrip pipeline runs at read-time:
- Secrets redacted first (catches
api_key,passwordpatterns in comments) - Comments stripped second (noise reduction for LLM)
- Secrets redacted first (catches
- Embeddings are cached to
.sf-assistant-embeddings.cachefor instant subsequent runs.
Handles 1000+ file codebases without running out of memory.
3. Hybrid Chunking Strategy
LLM context windows are limited. Youβre always balancing granularity vs. completeness, and the wrong call in either direction hurts retrieval quality. So I went with size-aware structural chunking.
- Small files (<1000 tokens): indexed as single CLASS unit
- Large files: split into CLASS_HEADER + individual METHOD units
- Huge methods (>1,500 tokens): further split into overlapping TEXT_CHUNK units (800 tokens, 160 overlap)
Returns specific methods rather than dumping entire files into context.
4. Call Graph-Based Dependency Expansion
Semantic search alone misses structurally related code, methods that call or are called by retrieved results. So the indexer builds a call graph and the context builder expands via BFS traversal.
- Detects method calls using regex patterns for instance/static/constructor calls
- Expands top-K semantic matches with their dependencies (configurable depth 1-3 hops)
- Applies score decay (0.7^depth) to prioritize direct matches over distant dependencies
More complete context without requiring manual /file pinning commands.
5. Multi-Provider LLM Support
Different LLM providers (OpenAI, Anthropic) have incompatible APIs. Provider abstraction with routing logic handles the differences.
HttpLlmClientroutes to provider-specific request builders (buildOpenAIRequestvs.buildAnthropicRequest)- Handles authentication differences (Bearer token vs. x-api-key header)
- Automatic token budget detection based on model name, deliberately conservative: the budgets are usable working room (~100k for Gemini, ~75k for Claude and GPT-4o class models), not raw context-window ceilings, which leaves headroom for the response and keeps retrieval focused
Switch between providers with a single config property change.
6. LLM Autonomous Tool Invocation
Users often need multiple pieces of context to answer a question, which without some automation means a lot of back-and-forth. So the LLM can autonomously invoke research tools during conversations.
When the LLM decides it needs more context, it outputs commands like:
COMMAND: /graph AccountService
COMMAND: /deps AccountTriggerHandler 2
SF-Assistant executes these and adds the results to the conversation, letting the LLM gather complete context before answering.
The loop runs governed, not open-ended: commands per turn are capped (5), research iterations are capped (3), duplicate invocations are suppressed, and every limit is configurable. The model gets tools, not free rein.
Dropped average turns per question from ~3 to ~1.5.
7. Smart Embedding Batching
Googleβs text-embedding-005 has strict limits (10 items per batch, ~20k tokens per batch). Intelligent batching respects both constraints, with an 18k-token budget per batch as safety margin under the hard limit.
if (batch.size >= maxBatchSize || batchTokens + unitTokens > maxTokensPerBatch):
submit batch
start new batch
- Small units (100 tokens): packs up to 10 items per batch
- Large units (5,000 tokens): uses only ~3 items per batch
- Mixed sizes: dynamically adjusts batch boundaries
Also supports automatic input_type handling for text-embedding-005 (search_document for indexing, search_query for queries).
Indexing runs are paced by a configurable rate limiter (delay plus jitter, max requests per minute), and a --test connectivity mode validates the API config on a handful of units before committing to a full index run.
Supported Metadata Types
The indexer handles 15+ Salesforce metadata types across three tiers:
Tier 0 (Apex): Classes, Triggers
Tier 1 (Declarative Logic): Flows, Validation Rules, Formula Fields, Custom Metadata
Tier 2 (UI Components): LWC, Aura, Visualforce, Page Layouts, Record Types
Each type has a specialized extractor that preserves semantic context (flow trigger types, validation rule formulas, etc.).
REPL Commands
Discovery & Navigation
| Command | Description |
|---|---|
/search <query> | Semantic code search with similarity scores |
/list [type] | Codebase inventory (all types or specific type) |
/graph <classname> | Show call relationships for a class |
/outline <classname> | Show class structure (methods, tokens, calls) |
/similar <filename> | Find semantically similar code units |
/stats | Show comprehensive codebase statistics |
Dependency Analysis
| Command | Description |
|---|---|
/deps <class> [depth] | Dependency tree visualization |
/callers <method> | Find all code that calls a specific method |
/flow [flowname] | Show flow definition or list all flows |
/trace <method> | Execution path tracing |
File & Method Operations
| Command | Description |
|---|---|
/file <name> | Pin a file for focused exploration |
/method <class.method> | Pin a specific method |
/usage <name> | Find where a class/method is used |
Debug Log Analysis
| Command | Description |
|---|---|
/profiling [path] | Parse profiling data from debug log |
/stacktrace [path] | Parse exception stack traces |
System Commands
| Command | Description |
|---|---|
/context | Debug what code was included in last query |
/tokens | Show token usage breakdown |
/dump [options] | Export code units to file with filtering |
/reindex | Re-scan project after code changes |
/clear | Clear conversation history |
Tech Stack
- Language: Java 11 (Standard Library Only)
- XML Parsing: StAX (javax.xml.stream)
- Web Server: com.sun.net.httpserver
- Build: javac + jar (no Maven/Gradle)
- Testing: 868 passing tests across 45 test classes, run by a dependency-free custom test harness (no JUnit, keeping the zero-dependency constraint honest even in the test suite)