Skip to content
Development

Building Greenhouse: the rules engine that came before the app existed

By Victor Da Luz
rusttaurisqlitedev-loggreenhouse

Greenhouse is a creative-project manager, but not the kind that just stores tasks. It enforces a process: capture ideas daily, rotate between projects instead of hyperfocusing on one, sit out a mandatory 7-day cooldown after each work session, never delete anything. The method is adapted from Mike Monday’s creative-process system in Make Music Your Life, generalized past music into a domain-neutral engine.

Day one was the PRD and the repo init. Day one was also GRNH-1: the rules engine. No UI existed yet. No screens, no windows, nothing to click. Just Rust.

That ordering was deliberate. The PRD I wrote that same morning describes the engine as “a state calculator, not a gatekeeper”: something that answers “what’s my situation right now?” and surfaces nudges, but never blocks an action outright. If cooldown logic, maturity timers, and vault rules all live behind a UI from the start, it’s too easy to let the interface quietly become the source of truth instead of the rules. Building the engine first, with no UI to lean on, forced every rule to be explicit and testable in isolation. In the Plane issue I wrote for it, I even marked it priority #1 “per stakeholder”, which was just me, but writing it that way kept the sequencing honest.

The commit that landed it is a full Tauri v2 scaffold wrapped around a pure Rust core:

  • config.rs: StageConfig and Config structs, with the “plant” template defaults baked in: 7-day cooldown, 7-day idea maturity, 14-day vault maturity, loaded from config.yaml.
  • state.rs: an ItemStatus enum, an Item type mirroring the DB row, and derive_item_state: a pure function that takes an item and a timestamp and returns its derived state. No side effects, no clock reads inside the function; the caller passes “now” in.
  • db.rs: the SQLite schema, two tables (items and touches), CRUD via rusqlite’s bundled build so there’s no system SQLite dependency to manage.
  • touch.rs: record_touch, the only function that starts a cooldown, plus vault_item and promote_idea.
  • daily.rs: derive_daily_state, which rolls everything up into a DailyState: the day’s worklist, which ideas are due, what needs a vault-review decision, whether anything’s been captured today, and a low-inventory warning if the pipeline’s running dry.

Twenty-two unit tests across those five modules, every one of them pinned to a fixed timestamp instead of calling the system clock. Cooldown and maturity logic is exactly the kind of code that looks correct and then flakes six months later because a test ran near a day boundary. Fixing “now” as an input, not an ambient global, sidesteps that from the first test.

Here’s the part I didn’t expect to be writing about: the commit message ends with “Requires rustup to build/test - Rust not yet installed on dev machine.” I wrote an entire rules engine, five modules, twenty-two tests, without being able to compile a single line of it. All from reading the PRD and thinking through the state transitions on paper.

The very next commit, 23 minutes later, is titled “fix(engine): all 22 tests passing”, which tells you how that went. Once Rust was actually installed, two things broke immediately. First, the Tauri bootstrap code wouldn’t compile for cargo test without app icons existing on disk, so the lib itself failed to build until I added placeholder RGBA icons. Second, the test database helpers were opening a real SQLite file inside a TempDir and letting the directory get dropped while the connection was still using it, a lifetime race that isn’t guaranteed to fail the same way twice. Switching every test to an in-memory database fixed it for good instead of papering over one flaky run. Neither problem showed up in the design on paper. Both only showed up once real Rust actually ran the tests.

Next up: the onboarding wizard shell, the first screen anyone will actually see. The engine’s been sitting there since day one, waiting for something to call it.

Related reading

Development

Setting up Claude Code for an iOS project

Writing the CLAUDE.md for a new iOS app before a line of Swift exists - the MusicKit-in-simulator trap, CloudKit's SwiftData rules, and why the learning-project framing came first.

Read