# 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<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`.

```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.
