Architecture
The rule
Section titled “The rule”
domain/imports nothing internal.application/imports onlydomain/.adapters/andentrypoints/may import anything.
What each layer is for
Section titled “What each layer is for”domain/ — pure logic
Section titled “domain/ — pure logic”Zod models and decision rules. No I/O, and no reading the clock: time arrives as an argument, so every function is a deterministic function of its inputs.
This is where “what counts as new”, “is this quote real”, “may this fire” and “is this watch due” live.
application/ — orchestration and ports
Section titled “application/ — orchestration and ports”The processing loop, due-filtering, the per-watch lock, and the port interfaces the loop depends on. It imports the domain and nothing else, and it performs no I/O of its own — it calls ports.
adapters/ — all input and output
Section titled “adapters/ — all input and output”Every implementation of every port: collectors, evaluators, embedders, notifiers, repositories, the mailer, auth, key pools and validation probes. If something touches the network, the disk or the clock, it belongs here.
entrypoints/ — runtime wiring
Section titled “entrypoints/ — runtime wiring”Two runtimes over one core:
| Entrypoint | What it is |
|---|---|
| CLI | run-due plus watch and user management commands |
| API | A Fastify app plus an in-process scheduler |
| Shared wiring | The single place the real adapter graph is assembled |
Both call the same wiring, which is what stops them drifting apart.
Data flow in one line
Section titled “Data flow in one line”Source → Collector → Candidate[] → (pure delta) → Evaluator → EvaluationResult → (pure validation + tiering) → Notification → Notifier → (pure state update) → RepositoryEvery arrow crossing into the outside world is a port; every transformation between them is pure.
Validation at the boundary
Section titled “Validation at the boundary”Zod schemas guard every external boundary: collector output, model output, YAML watch files, HTTP request bodies, and every document read from the database. A malformed item is rejected at the edge rather than propagating inward, and the inferred types are the single source of truth used across all four layers.
Why the two runtimes exist
Section titled “Why the two runtimes exist”The monitoring is a short-lived outbound job — perfect for a scheduled runner and free of hosting cost. The API is a long-lived service that only exists for management. Keeping the core independent of both means neither constrains the other, and either can be replaced without touching decision logic.
Where to look next
Section titled “Where to look next”- Repository structure — the file map.
- Ports and adapters — the interfaces.
- Watcher internals — the loop in detail.