# Theseus Coding Conventions

> Extracted from `src/types.rs`, `src/db.rs`, `src/lib.rs`, `src/routes/*.rs`, `src/main.rs`, `src/agent/*.rs`

## 1. Axum Route Syntax — `:param` Only

Use `:param` syntax in all axum routes. `{param}` silently 404s.

```rust
.route("/api/projects/:project_id/nodes", post(create_node).get(list_nodes))
.route("/api/projects/:project_id/nodes/:node_id", get(get_node).put(update_node).delete(delete_node))
```

Multi-segment: `Path((project_id, node_id)): Path<(String, String)>`.

## 2. Route Module Pattern

Each domain: `src/routes/<entity>.rs` with `pub(crate) fn router() -> Router<AppState>`. Merged in `src/lib.rs`. 15 modules: agent, analysis, chat, check, credentials, daily_report, edges, evidence, import, nodes, projects, report, search, timeline, ws.

## 3. `NodeType::Custom` — Untagged Serde Ordering

`#[serde(untagged)]` variant MUST come LAST in any enum. Serde tries in order — `Custom(String)` would match everything if placed before named variants. Every variant also needs a `Display` impl.

## 4. `ApiResponse<T>` Wrapper

All endpoints return `ApiResponse<T>`. Key constructors:

```rust
Ok(Json(ApiResponse::ok(data)))          // success: true
// Error via helper:
state.db.method().map_err(|err| crate::db_error_status("op", "entity", err))?;
```

## 5. `AppError` — Structured HTTP Errors

```rust
pub(crate) struct AppError {
    pub(crate) status: StatusCode,
    pub(crate) code: &'static str,         // dot-notation: "db.not_found"
    pub(crate) message_en: String,
    pub(crate) message_zh: String,          // bilingual — always both
    pub(crate) operation: &'static str,     // e.g. "evidence.upload.create_dir"
    pub(crate) entity: Option<&'static str>,
    pub(crate) field: Option<&'static str>,
    pub(crate) hint: Option<String>,
    pub(crate) source: Option<String>,
}
```

Builder: `.with_entity()`, `.with_field()`, `.with_hint()`, `.with_source()`.

`AppError::from_db()` auto-maps SQLite errors → HTTP status: no rows → 404; extended codes 787/1555/2067 → 409; else → 500. `db_error_status()` in `src/lib.rs` is the standard conversion helper.

## 6. Mutex\<Connection\> with Poison Recovery

```rust
fn c(&self) -> std::sync::MutexGuard<'_, Connection> {
    self.conn.lock().unwrap_or_else(|e| e.into_inner())
}
```

All DB ops use `self.c()`. The `unwrap_or_else` recovers from poison.

## 7. Hardcoded Column Indices

Row deserializers use positional `row.get(N)` (0-based). SELECT column reorder = **silent bug**. When adding columns: append to SELECT + use next index.

## 8. CST Timezone for Dates

User-facing dates use CST (UTC+8), not server local: `chrono::FixedOffset::east_opt(8 * 3600)`.

## 9. Static Asset Handling

`index.html` compiled via `include_str!` (requires rebuild). CSS/JS served at runtime via `ServeDir` (no rebuild).

## 10. Evidence File Paths

Pattern: `data/{pid}/evidence/{nid}/{uuid}.{ext}`. DB holds metadata only — deleting the DB record does NOT delete the file.

## 11. Other Conventions

- **Type abbreviations:** `entry`/`ep`, `obj`, `recon`, `privesc`, `vuln`, `cred`, `svc`, `net`, `exfil`, `exploit`.
- **`From<Entity>` for responses:** All response types stringify enum fields.
- **Conventional commits:** `feat:` / `fix:` / `refactor:` / `docs:` / `chore:`. No AI tags.
- **Custom type regex:** `[a-z][a-z0-9_]{0,29}` validated by `is_valid_custom_type()`.
- **Timestamps:** RFC3339 TEXT in SQLite, `DateTime<Utc>` in Rust.
- **Short IDs:** API accepts UUID prefix ≥6 chars (via `resolve_*_id` helpers).
- **WebSocket:** After mutations, broadcast `graph:<pid>`. Frontend reloads graph.

## 12. Agent Module Architecture

`src/agent/` — autonomous AI subsystem:

```
loop.rs     — AgentRuntime + agentic loop (max 5 turns)
llm.rs      — LlmClient (DeepSeek API, OpenAI-compatible)
memory.rs   — MemoryStore (persists turns to DB by project_id + run_id)
prompts.rs  — system prompt construction
tools.rs    — tool registry + dispatch
```

`AgentRuntime` owns `LlmClient`, `MemoryStore`, `broadcast::Sender<String>`. Loop: context → system prompt → load history → LLM call → execute tools → repeat until no tools or max turns.

## 13. Agent Error Hierarchy

Agent uses **plain enums**, not `AppError`:

```rust
pub enum AgentError { LlmError(LlmError), MemoryError(String), DatabaseError(String) }
pub enum LlmError { RequestFailed(String), ApiError { status: u16, message: String }, ParseError(String) }
pub struct ToolError(pub String);  // in tools.rs
```

Route handler (`src/routes/agent.rs`) converts to `StatusCode::INTERNAL_SERVER_ERROR`. **Rule:** Agent internals never touch `AppError`.

## 14. Agent Tool Pattern

Tools registered in `registry()` → `Vec<Tool>`. Each: `name`, `description`, `parameters` (JSON Schema), `execute: fn(&ToolContext, Value) -> Result<ToolResult, ToolError>`.

`ToolContext` = `{db, project_id, tx}`. Results include `side_effects: Vec<SideEffect>` (`NodeCreated`, `EdgeCreated`, `CredentialCreated`).

**Adding a tool:** implement `create_<name>_tool()`, add to `registry()`, define JSON Schema inline.

## 15. Code Comments Language

`src/agent/` — Chinese. `src/routes/`, `src/lib.rs`, `src/db.rs` — English. Match the surrounding file.
