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.