Project

dogfood

0.0
The project is in a healthy, maintained state
Document the real scenarios your product will face, generate reproducible walkthrough scripts, and walk through your app to find UX gaps. Domain-agnostic; content supplied via packs.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
>= 6.0
~> 13.0
~> 3.0
 Project Readme

dogfood

A scenario walkthrough engine for UX iteration.

dogfood helps you find UX blind spots before your users do. You document the real scenarios your product will face — a new user signing up, a customer cancelling a subscription, a support ticket escalating — and dogfood turns them into reproducible walkthrough scripts. You follow the script through your app, spot where it breaks, fix it, and iterate.

It works for anything with a user pursuing an end goal: a CLI, a TUI, a web app, a mobile app, an API. The gem knows nothing about your domain; you supply the content via a pack. See SPEC.md for the full contract.

How it works

  1. Document a scenario — write a YAML file (or a Ruby class) describing the steps a user takes to reach a goal, including the branches and edge cases they might hit.
  2. Generate a walkthrough — run the CLI. dogfood picks scenarios, walks them through their stages, and writes a Markdown log of every action.
  3. Walk through your app — open the log and follow it, clicking through your real product exactly as the script describes.
  4. Find gaps and iterate — where the script doesn't match reality, or the flow feels broken, that's a UX bug. Fix it, re-run with the same seed, and compare.

Because runs are seeded, you can reproduce the exact same walkthrough for a teammate or after a fix.

Install

gem install dogfood

Or add to a Rails Gemfile:

gem "dogfood"

Quick start (plain Ruby)

Write a pack file that registers a step module and loads scenarios:

require "dogfood"

Dogfood::Pack.current = Dogfood::Pack.new(:app) do |p|
  p.include_steps MySteps
  p.load_dir "scenarios/*.yml"
end

Then run the CLI:

ruby -r dogfood -e 'Dogfood::CLI.new(["--pack","my_pack.rb","--list"]).run'

Quick start (Rails)

  1. Add gem "dogfood" to the Gemfile.
  2. Run bin/rails g dogfood:install to scaffold config/dogfood.rb and config/dogfood/.
  3. Edit config/dogfood.rb to register your step modules.
  4. Drop scenarios into config/dogfood/*.yml.
  5. Run bin/rails dogfood --list and bin/rails dogfood --days 3.
  6. Run bin/rails dogfood:smoke to run the YAML smoke test.

Running the bare dogfood executable from a Rails app root also works. The CLI detects the app via config/application.rb and exposes Rails.root (a minimal shim, no full boot) so config/dogfood.rb can use Rails.root.join(...) as usual.

A scenario, end to end

Here's a small, domain-agnostic example: a new user signing up for a SaaS product and reaching their first value moment. It's not about auto repair or work orders — it's about any user with a goal.

name: signup_to_first_report
title: Signup to First Report
stages: [landing, signup, onboarding, first_action, value_moment]

branches:
  landing:
    organic: 0.60
    referral: 0.25
    paid_ad: 0.15

stages_def:
  landing:
    - call: visit_landing
      when_branch: organic
      with:
        source: "search"
      out: visitor
    - call: visit_landing
      when_branch: referral
      with:
        source: "referral"
      out: visitor
    - call: visit_landing
      when_branch: paid_ad
      with:
        source: "ad"
      out: visitor
    - call: view_pricing
      with:
        plan: ${visitor.plan}

  signup:
    - call: fill_signup_form
      with:
        email: "alex@example.com"
      out: account
    - call: verify_email
      with:
        email: ${account.email}
    - call: create_workspace
      with:
        name: "Acme Inc"
      out: workspace

  onboarding:
    - call: choose_plan
      with:
        plan: ${visitor.plan}
      out: plan_choice
    - when: ${plan_choice == :self_serve}
      then:
        - call: start_self_serve_onboarding
      else:
        - call: book_sales_call
          with:
            rep_name: "Jordan"
    - call: connect_data_source
      with:
        source: "csv"
      out: data_source

  first_action:
    - call: run_first_report
      with:
        data_source: ${data_source}
      out: report
    - maybe: 0.2
      then:
        - call: hit_data_error
          with:
            report: ${report}
            error: "column mismatch"
    - call: share_report
      with:
        report: ${report}
        channel: "email"

  value_moment:
    - call: see_insight
      with:
        report: ${report}
      out: insight
    - call: invite_teammate
      with:
        email: "sam@example.com"
    - call: update_state
      with:
        status: activated
        substatus: "First report shared"

The call: names (visit_landing, fill_signup_form, ...) are methods on your step modules — the gem doesn't define them. You implement each one to record a UI action and return data. See EXTENDING.md for how.

Run it:

dogfood --pack my_pack.rb --scenario signup_to_first_report --seed 42

dogfood writes a Markdown walkthrough to tmp/dogfood/. Each story is a numbered, actor-labeled log of actions:

# Walkthrough Log

## Day 1 — 2026-08-06

### Summary
- New stories today: 1
- Carry-overs: 0

### Morning

#### S-001
**Status:** activated
**Substatus:** First report shared
1. **Visitor** — Visit landing page
   - **Action:** Open /pricing from search
2. **Visitor** — View pricing
   - **Action:** Select plan
...

The explain command

Inspect a scenario without randomness — a static sanity check of the scenario definition:

dogfood --pack my_pack.rb --explain signup_to_first_report
dogfood --pack my_pack.rb --explain scenarios/example.yml
dogfood --pack my_pack.rb signup_to_first_report --explain

Each compiles the scenario and prints its stage/decision tree without invoking any RNG.

Days are optional

Most scenarios complete in a single pass — --days 1 is the default and is perfectly fine. Multi-day only matters when a scenario pauses and resumes, e.g. "waiting for an external event" (a part to arrive, an approval, a payment). If your scenario doesn't pause, you don't need to think about days at all.

The depth-1 nesting limit

when and maybe blocks may only contain plain call steps — no nested conditionals (this is enforced and documented in SPEC.md §4.7). Treat it as a feature: keep scenarios shallow and declarative. If a story needs deeper logic, write it as a Ruby Dogfood::StoryBase subclass instead.

Extending the gem

See EXTENDING.md for the three extension tiers: adding domain behavior (host app, no gem changes), new YAML step constructs, and new core engine classes.

Configuration

Flag Description
--pack PATH Ruby file that registers a Dogfood::Pack (default: config/dogfood.rb in the current directory)
--scenario NAME Run a specific named scenario (otherwise: random mix)
--days N Number of simulated days (default: 1)
--stories-per-day N or M-N Stories per day (default: 3-6)
--seed N Set random seed for reproducibility
--random Use a random seed
--output PATH, -o PATH Output file path
--list, -l List available scenarios
--explain [NAME|FILE] Print the stage/decision tree for a scenario name or YAML file, no RNG
--dry-run --scenario N Same as --explain but takes a registered name
--help, -h Show help

Development

bundle exec rake test

Requires Ruby >= 3.1.

License

MIT. See LICENSE.txt.

Full spec

SPEC.md