ADR-0008: Structure ai-coach as ports and adapters, enforced by Nx module boundaries
Status: accepted · 2026-10-24
Context
The first build split the code by feature (vault-client, sparring-partner, external-judge, scheduler, coach), and business rules sat next to Ollama, Gemini and MCP code. Swapping a model or the vault transport would have touched the rules, and nothing stopped a rule from importing an SDK. The restructure followed ten references on hexagonal architecture, DDD and Nx boundaries (Cockburn, Graça, Sairyss's domain-driven-hexagon, the Netflix case study, Angular Atlas, Marmicode, the Nx docs and others).
Decision
-
The workspace is an Nx monorepo on npm workspaces, TypeScript, Vitest and ESLint, with seven projects:
Project Tags Holds domaintype:domainRules that guard themselves: SparringRound, sparring rules, buzzword and verbatim checks, the review ladder, the verdict rule,tier2Payload()applicationtype:applicationUse cases ( Sparring,SparringConversation,Judging,Review,Filing) and the ports they needadapter-vault-mcptype:adapter,adapter:vault-mcpMcpVaultover kb-template's MCP serveradapter-ollamatype:adapter,adapter:ollamaOllamaSparringPartner, prompts and the loopback guardadapter-geminitype:adapter,adapter:geminiGeminiJudgeand the judge protocoladapter-local-statetype:adapter,adapter:local-stateThe pending-judgement queue and the clock clitype:appTerminal, config, commands and composeCoach, the composition root -
Dependencies point inward. Ports are TypeScript interfaces shaped by what the use cases need; there is no DI container, so the composition root binds them by hand.
-
Adapters translate technology failures into application errors (
UnavailableError,UnusableReplyError,VaultRefusedError); the CLI maps those to messages and exit codes. -
@nx/enforce-module-boundariesenforces it onlint: inward-only tags, no adapter importing another adapter, no circular dependencies, and anallowedExternalImportswhitelist per tag (none fordomainandapplication; only its own SDK,zodand Node for each adapter). -
no-restricted-importsbans Node built-ins indomainandapplication, which the whitelist does not cover. -
ARCHITECTURE.mddescribes the layout; this ADR records why.
Consequences
- A new model or transport is a new adapter plus one line in
composeCoach. - Each boundary rule was checked by planting a violation and seeing lint fail.
- CI already runs
lint, so the rules hold there too.