Discord bot inbound adapter (adapters/inbound/discord_bot.py):
- ChatService injected directly — no HTTP roundtrip to FastAPI API
- No module-level singleton: create_bot() factory for construction
- Pure functions extracted for testing: build_answer_embed,
build_error_embed, parse_conversation_id
- Uses message.reference.resolved cache before fetch_message
- Error embeds never leak exception details
- 19 new tests covering embed building, footer parsing, error safety
Removed old app/ directory (9 files):
- All functionality preserved in hexagonal domain/, adapters/, config/
- Old test_basic.py removed (superseded by 120 adapter/domain tests)
Other changes:
- docker-compose: api uses main:app, discord-bot uses run_discord.py
with direct ChatService injection (no API dependency)
- Removed unused openai dependency from pyproject.toml
- Removed app/ from hatch build targets
Test suite: 120 passed, 1 skipped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Domain layer (zero framework imports):
- domain/models.py: pure dataclasses (RuleDocument, RuleSearchResult,
Conversation, ChatMessage, LLMResponse, ChatResult)
- domain/ports.py: ABC interfaces (RuleRepository, LLMPort,
ConversationStore, IssueTracker)
- domain/services.py: ChatService orchestrates Q&A flow using only ports
Outbound adapters (implement domain ports):
- adapters/outbound/openrouter.py: OpenRouterLLM with persistent httpx
client, robust JSON parsing, regex citation fallback
- adapters/outbound/sqlite_convos.py: SQLiteConversationStore with
async_sessionmaker, timezone-aware datetimes, cleanup support
- adapters/outbound/gitea_issues.py: GiteaIssueTracker with markdown
injection protection (fenced code blocks)
- adapters/outbound/chroma_rules.py: ChromaRuleRepository with clamped
similarity scores
Inbound adapter:
- adapters/inbound/api.py: thin FastAPI router with input validation
(max_length constraints), proper HTTP status codes (503 for missing LLM)
Configuration & wiring:
- config/settings.py: Pydantic v2 SettingsConfigDict (no module-level singleton)
- config/container.py: create_app() factory with lifespan-managed DI
- main.py: minimal entry point
Test infrastructure (90 tests, all passing):
- tests/fakes/: in-memory implementations of all 4 ports
- tests/domain/: 26 tests for models and ChatService
- tests/adapters/: 64 tests for all adapters using fakes/mocks
- No real API calls, no model downloads, no disk I/O in fast tests
Also fixes: aiosqlite version constraint (>=0.19.0), adds hatch build
targets for new package layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>