Skip to content

Internals

This page is the bridge from behaviour to source. It assumes you have read How it works.

  • 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

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.

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 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.

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.

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.

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

The loop is closed for modification and open at the ports. Adding a source type or a delivery channel does not touch runWatch.ts: