# 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 ```bash # 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`. ```java // 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). ```java // 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. ```java // 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. ```java // Correct var session = chatService.createSession(binaryId, projectId, model); var messages = new ArrayList(); 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`. ```java @Service @RequiredArgsConstructor public class MyService { private final DependencyA depA; private final DependencyB depB; } ``` ### Transactions - `@Transactional` on 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: ```java 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=validate` only ## Design Principles - **DRY** — Extract repeated logic. The refactoring of `ChatController.sendMessage()` is the canonical example: duplicated error formatting was extracted into reusable `event()` and `errorEvent()` 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 `@NamedInterface` for fine-grained intra-module access control.