Skip to content

Engineering principles

The "why" that sits above ADRs. ADRs capture specific decisions; this page captures the shape of the decisions — what we lean toward and what we lean away from when judgement calls come up.

What we lean toward

1. Honest defaults, not magic

Code should do what it looks like it does. No hidden middleware altering responses, no decorators rewriting function bodies, no "convention over configuration" that means "you have to know the convention."

Concretely: routes are functions. State is in URL params. Database access goes through Prisma. If you're surprised by behaviour, the behaviour is wrong.

2. Server-rendered first

We default to RSC + server actions. Client islands are opt-in for things that need them (Framer Motion, drag-and-drop, real-time UI). Client-state libraries (Zustand, etc.) are deliberately absent — URL state and server state cover almost everything.

Concretely: the build page state is in URL params. No Zustand store. The URL is the build.

3. Small modules, plain functions

src/lib/* and src/rules/* are mostly stateless functions. Easy to test, easy to swap, easy to reason about. Classes show up only when truly needed (and rarely).

4. Tests close to the code

Vitest specs in tests/ mirror src/. A bug fix lands with a regression test in the same PR. Coverage isn't a target — meaningful tests are.

5. Documentation alongside code

When architecture changes, the docs in docs/architecture/ change in the same PR. Stale architecture docs are worse than no docs.

6. Concrete over generic

A function called matchCpu beats a function called match with a category enum dispatch. Keep generic abstractions until 3+ concrete cases prove the pattern.

What we lean away from

1. Generic AI aesthetics

The v0.1 prototype was rejected for this; see postmortem. Default Tailwind palettes, system-only fonts, "step 1 of 2" wizards — all signs that the design didn't try.

Concretely: every visual choice should have a reason. Cedar green is Lebanese. Heat orange marks operator/admin. Petrol black sets the premium-hardware tone.

2. ORM relations for cross-aggregate writes

Prisma's include and connect are fine for reads and for writes within an aggregate (e.g., creating a Listing with its first ListingPrice). For writes across aggregates (e.g., bulk-updating Products from a CSV), prefer explicit transactions and explicit SQL.

3. Silent error handling

Catching an error and returning null or empty hides problems. Either:

  • Handle it meaningfully — log + recover + tell the user what happened
  • Let it propagate — the framework's error boundary will catch it

A catch { } is almost always wrong.

4. Premature abstractions

If you have one scraper, you don't need a Scraper interface. When you have three scrapers and find yourself copy-pasting helper logic between them, then extract to src/scrapers/core/. Three is the trigger, not two.

5. Configuration knobs without callers

Don't add a MATCHER_CONFIDENCE_THRESHOLD env var "in case we want to tune it." Hardcode the threshold; if it ever needs tuning, the diff that needs to flip it can introduce the env var at the same time.

6. --no-verify, --force-push to master, taskkill /f node.exe

Hooks exist for reasons. Force-pushing master destroys other people's work (even your other-machine work). Killing all Node processes kills your editor session. Captured in Contributing and Onboarding.

Trade-off compass

When two principles conflict, here's the rough weighting:

If you have to choose Lean Reason
Speed-to-ship vs. quality polish Quality polish The v0.1 lesson — generic ships fail review, and we own time across days, not hours
Generic flexibility vs. concrete fit Concrete fit Generic costs upfront and only pays off when used. Most never pay off
Save-the-keystroke vs. clarity Clarity Future-you reading the code 6 months later thanks present-you
Centralised state vs. URL state URL state URLs are shareable, refresh-survivable, and need no library
Custom solution vs. boring library Boring library Until the library doesn't fit, then write your own
Big PR vs. many small PRs One coherent PR per ticket Squash-merged for clean history; smaller PRs sometimes appropriate, but most work is one ticket = one PR

Where this came from

The principles aren't aspirational — they're the patterns that emerged in the v1.0 rebuild after v0.1 was rejected. They'll evolve as the project teaches us new things; if a principle becomes wrong, that's worth its own ADR.