Comprehensive documentation for the AI module covering architecture, API design, data model, RAG pipeline, configuration, integration points, and frontend integration guide. Includes IDA5 extraction points reference and AGENTS.md with project conventions and coding standards.
6.4 KiB
AGENTS.md — decompile-ai
Project Overview
decompile-ai is a backend service for binary analysis automation. It accepts uploaded binaries, runs them through multiple analysis engines (IDA Pro, Ghidra, DiE) inside Docker containers, and provides AI-powered chat over the decompiled code via Ollama + RAG (pgvector embeddings).
- Java 25, Spring Boot 4.0.6, Maven wrapper (
./mvnw) - PostgreSQL + Flyway migrations; H2 in-memory for tests
- RabbitMQ for async job dispatch
- Spring Modulith for module boundaries, event publication, and architecture verification
- Spring AI (Ollama) for chat and embedding generation
- Spotless (Google Java Format) for code formatting
Build & Run Commands
# Compile
./mvnw compile
# Run tests
./mvnw test
# Run a specific test class
./mvnw test -Dtest=ChatServiceTest
# Package
./mvnw package -DskipTests
# Run the app locally (needs PostgreSQL + RabbitMQ)
./mvnw spring-boot:run
# Format code
./mvnw spotless:apply
# Check formatting (CI-style)
./mvnw spotless:check
Project Structure
src/main/java/ai/decompile/
├── DecompileAiApplication.java # Spring Boot entry point
├── WebConfig.java # CORS configuration
├── workspace/ # Workspace, Project, Binary CRUD
│ ├── controller/
│ ├── event/ # Domain events (BinaryUploadedEvent)
│ ├── model/{dto,entity,repository,specification}/
│ └── service/
├── analysis/ # Static analysis results (functions, xrefs, labels)
│ ├── controller/
│ ├── event/ # StaticAnalysisRequestedEvent, StaticAnalysisCompletedEvent
│ ├── model/{dto,entity,repository}/
│ └── service/
├── engine/ # Analysis engines: IDA v5, Ghidra (Docker-managed)
│ ├── config/
│ ├── model/
│ └── service/
├── die/ # DiE (Detect It Easy) file type detection (Docker)
├── docker/ # Docker container management via docker-java
├── job/ # Async job queue: create, dispatch, execute, track status
│ ├── controller/
│ ├── messaging/ # RabbitMQ publisher + listener
│ ├── model/{dto,entity,enums,repository,specification}/
│ └── service/handler/ # JobType handlers (AnalyzeFile, StaticAnalysis, Embedding)
├── ai/ # AI chat + RAG embeddings (Spring AI + Ollama)
│ ├── config/
│ ├── controller/
│ ├── model/{dto,entity,jdbc,repository}/
│ └── service/
└── common/ # Shared config and exceptions (NotFoundException, ConflictException)
└── exception/
Coding Conventions
Null Checking
Always use java.util.Objects.isNull() and java.util.Objects.nonNull() — never use raw == null or != null.
// Correct
if (Objects.isNull(value)) { ... }
if (Objects.nonNull(value)) { ... }
// Wrong
if (value == null) { ... }
if (value != null) { ... }
If Statements
Always use braces, even for single-line bodies. Always include a blank line after the closing } of an if block (before the next statement).
// Correct
if (condition) {
doSomething();
}
nextThing();
// Wrong
if (condition) doSomething();
// Wrong (no blank line after if)
if (condition) {
doSomething();
}
nextThing();
Classes and Records
No blank line between the class/record opening brace and the first member.
// Correct
public class Job {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// Correct
public record ChatRequest(@NotBlank String content) {}
// Wrong (blank line after brace)
public class Job {
@Id
private UUID id;
Type Inference (var)
Use var for local variables whenever the type is obvious from the right-hand side.
// Correct
var session = chatService.createSession(binaryId, projectId, model);
var messages = new ArrayList<Message>();
var binary = binaryService.getBinary(binaryId);
// Acceptable (type not obvious from RHS, or lambda target typing needed)
StreamingChatModel streamingModel = resolveStreamingChatModel(session.getChatModel());
Dependency Injection
Constructor injection via Lombok @RequiredArgsConstructor. Declare dependencies as private final.
@Service
@RequiredArgsConstructor
public class MyService {
private final DependencyA depA;
private final DependencyB depB;
}
Transactions
@Transactionalon write operations@Transactional(readOnly = true)on read-only queries@Transactional(propagation = Propagation.REQUIRES_NEW)for independent sub-transactions
Testing
- JUnit 5 + Mockito (unit tests)
- MockMvc + H2 (integration tests)
- Unit tests:
@ExtendWith(MockitoExtension.class),@Mock/@InjectMocks - Integration tests:
@SpringBootTest,@ActiveProfiles("test") - Test class naming:
*Test.java(unit),*IntegrationTest.java(integration) - Static imports for assertions:
assertEquals(...),assertThrows(...),assertTrue(...)
Logging
Lombok @Log4j2 on all non-trivial classes. Use parameterized messages:
log.info("Processing binary={} with engine={}", binaryId, engineName);
log.error("Failed to stream chat for session {}", sessionId, error);
Domain Events (Spring Modulith)
Event listeners use @ApplicationModuleListener. Events are published via ApplicationEventPublisher.publishEvent().
Flyway Migrations
- Files in
src/main/resources/db/migration/, naming:V001__description.sql - DDL changes go through Flyway; Hibernate
ddl-auto=validateonly
Design Principles
- DRY — Extract repeated logic. The refactoring of
ChatController.sendMessage()is the canonical example: duplicated error formatting was extracted into reusableevent()anderrorEvent()helpers. - SOLID — Each module (workspace, analysis, engine, job, ai) has a single responsibility. Dependencies between modules are explicitly declared in
package-info.java. - Clean Code — Small methods, meaningful names, no comments explaining what (the code does that), only comments explaining why when non-obvious.
- Module boundaries — Cross-module communication via domain events, not direct service calls. Use
@NamedInterfacefor fine-grained intra-module access control.