Internals
This page is the bridge from behaviour to source. It assumes you have read How it works.
Where the engine lives
Section titled “Where the engine lives”Directorysrc/
Directorydomain/
- decisions.ts hashing, delta, fingerprints, grounding, tiering, cron
- models.ts Zod schemas and trust resolution
Directoryapplication/
- runWatch.ts the loop itself
- executeWatch.ts loop + run record + last-run, under the lock
- runDueWatches.ts load, filter due, run through a small worker pool
- watchLock.ts in-process per-watch mutual exclusion
- ports.ts the interfaces the loop depends on
Directoryadapters/ every implementation of those ports
- …
Directoryentrypoints/
- wiring.ts builds the real adapter graph for both runtimes
The layering rule
Section titled “The layering rule”domain/ imports nothing internal. application/ imports only domain/.
Adapters and entrypoints may import anything. That is what allows the same loop
to run as a scheduled CLI job and as an always-on service without modification —
and it is why the decision rules can be tested with no network and no API keys.
The loop performs no I/O of its own: it orchestrates ports.
Pure versus impure
Section titled “Pure versus impure”Everything that decides is pure and lives in domain/decisions.ts:
| Function | Decides |
|---|---|
candidateHash, computeDelta |
What counts as new content |
canonicalEventAnchor, eventFingerprint |
What counts as the same event |
isEvidenceGrounded |
Whether a quote is real |
validateEvidence |
Whether the URL and date are real |
corroboratingTrust, tierAllowsStrong |
Whether the source may confirm |
combineOpinions |
How two model verdicts combine |
mergeChunkVerdicts |
How chunked evaluations reduce to one verdict |
classifyAlert |
The final strong / weak / none call |
isDue |
Whether a watch should run now |
These take time as an argument rather than calling the clock, so their behaviour is fully reproducible in tests.
The dependency object
Section titled “The dependency object”The loop receives everything it needs as a plain object: collectors keyed by
source type, an evaluator, notifiers keyed by channel, a repository, and
optionally an embedder and a lock. entrypoints/wiring.ts is the single place
the real graph is assembled, and both runtimes call it — which is what stops the
CLI and the service drifting apart.
Concurrency model
Section titled “Concurrency model”Watches are processed sequentially by default; a bounded worker pool is available
via RUN_CONCURRENCY, which is kept at 1 by default because the key pools are
shared and rate-limited.
Persistence contract
Section titled “Persistence contract”Run state is one document per watch. Every optional field of the state model must be handled in both directions by the repository adapter — a field the document type omits is silently dropped on write and silently absent on read, which turns a durable feature into an in-memory-only one.
Where each stage is implemented
Section titled “Where each stage is implemented”| Stage | Implementation |
|---|---|
| Redelivery | redeliverPending in runWatch.ts |
| Collection | Collector adapters, called in source order |
| Delta | computeDelta |
| Evaluation | CascadeEvaluator over KeyRotatingEvaluator over LlmEvaluator |
| Evidence validation | validateEvidence |
| Tiering | classifyAlert |
| Semantic dedup | Embedder port + maxCosineSimilarity |
| Delivery | Notifier adapters, one per channel |
| Persistence | Repository.saveState and recordRun |
Extending it
Section titled “Extending it”The loop is closed for modification and open at the ports. Adding a source type
or a delivery channel does not touch runWatch.ts: