Skip to content

Working in the pure core

The domain layer is where every decision lives. It is also the easiest layer to spoil, so it has hard rules.

Rule Why
No I/O. No network, no filesystem, no database The layer must be testable and portable
No Date.now(). Time arrives as an argument Deterministic tests, and reproducible decisions
No randomness Same input, same output, always
No imports from adapters/ or entrypoints/ The dependency rule
Total functions. Handle the empty case, the missing case, the malformed case The loop calls these on real-world data

Models — Zod schemas for every domain object plus the inferred types. These are the single source of truth: the API validates against them, the panel imports them, and the database parses every document through them.

Decisions — content hashing and delta, event identity, evidence grounding and validation, trust resolution, verdict combination, chunk merging, alert classification, cosine similarity, cron due-filtering.

  1. Write the test first, including the case you are fixing and the cases that must not regress.
  2. Keep the function pure. If you need new information, add a parameter — do not reach for a global or a clock.
  3. Default to the old behaviour when new information is absent. Every optional signal in the current tiering logic follows this pattern: only an explicit negative demotes, so a caller that knows nothing is unaffected.
  4. Check both directions. A stricter rule reduces false positives and raises false negatives. Say which you intended.
  5. Run the accuracy harnesspnpm eval — to see the effect on the golden set before and after.
  1. Add the field to the Zod schema as optional with a safe default, so existing stored documents still parse.
  2. If it is part of run state, handle it in both repository implementations.
  3. If it changes an identity — a content hash or an event fingerprint — keep recognising the old form alongside the new one, or the upgrade produces a flood of duplicate alerts or a wave of re-evaluation.
  4. Update the watch schema documentation.
  • Every exported function carries a comment explaining why it exists, not what it does. The code says what.
  • Prefer many small pure functions over one large one: the loop composes them, and each is independently testable.
  • Name things after the decision, not the mechanism — classifyAlert, not checkThresholds.

Domain tests are plain unit tests with no setup:

it('denies a strong alert when the source is unrated', () => {
expect(tierAllowsStrong(undefined)).toBe(false)
expect(tierAllowsStrong(undefined, true)).toBe(true)
})

The rules file has by far the densest test coverage in the repository, and that is deliberate: it is where correctness is cheapest to establish and most expensive to lose.