Next, we ask the AI to split the tests in batches, enable these batches one by one, and generate the code so that it passes the tests for that batch. The AI will base the code on the building blocks - and their related instructions - that are contained in the composable solutions.
Among these instructions, the most important one is to create (what I call) focussed feature-packages that contain focussed files. A feature-package encapsulates one particular functionality or concept of the code-base, such as “hotel_rooms” or “bookings”. When you read out the list of feature-packages, then it should sound like a high-level description of your application. Within a feature-package, the code is split into models (plain data-structures, almost no logic), actions (functions that change the application state), rules (functions that query the application state), utils, and a few other (optional) categories. By insisting that the code is split up in this way, we counter the tendency of the AI agent to keep adding more features to the same files (which leads to slop code).
The AI agent does a great job at creating focussed feature-packages and files, but it will sometimes create a large action file that does the work of an entire feature-package. When this happens, it’s because a feature-package was missing. I identify these cases by listing the n biggest python files in the code base, and ask the agent to split up the big file by introducing a new feature-package (with a name suggested by me).
At this point, our application requirements are encoded in the test-suite, but it would still be nice to have a user-friendly high-level description of the features. Neither the design doc nor the workflow-tests serve that purpose very well. Therefore, I ask my AI to create scenario documents that explain the main features and workflows, while leaving out the edge-cases and details (an interested user can consult the workflow-tests for those). As the scenarios are created, the related parts are deleted from the design doc (until eventually the design doc is empty).
For capturing scenarios I use a special tool called sunya, but you can of
course create your own format. My scenarios start with a run_scenario function that explains a workflow by
walking through pseudo-code. Next, there is a Markdown description of the scenario steps, followed by a sequence
diagram. All these three components overlap, but they also complement each other in nice ways. Below is the
scenario for the reminders of our todo app, and the Markdown document that sunya generates from it.
import sunya
from sunya import C as c
from sunya import R as r
from sunya import goal, it, the
scenario = sunya.add_scenario("s03_a_user_is_reminded_of_overdue_todos")
def run_scenario():
r.user = c.External.Actors.User
r.scheduler = c.External.Systems.Scheduler
r.send_reminders_action = c.Reminders.Actions.SendReminders
r.get_todos_to_remind_rule = c.Reminders.Rules.GetTodosToRemind
r.reminder = c.Reminders.Models.Reminder
r.user_settings = c.Users.Models.UserSettings
with goal().remind_the(r.user).of_their(r.overdue_todos):
# The reminder job sends the reminders
with the(r.scheduler).runs_the(r.send_reminders_action).every(r.morning):
with it().visits_each(r.user).who_enabled_reminders_in_their(
r.user_settings
):
it().skips_the(r.user).if_there_is_a(r.reminder).of_today()
it().calls(r.get_todos_to_remind_rule).to_find_the(r.overdue_todos)
it().sends_a(r.reminder).that_lists_the(r.overdue_todos)
it().stores_the(r.reminder)
# The user acts on the reminder
with the(r.user).reads_the(r.reminder):
it().marks_a(r.todo).as_done().or_changes_its(r.due_date)
markdown_node = sunya.add_markdown_node(
scenario, "A user is reminded of their overdue todos"
)
markdown_node.markdown = """
A user who forgets about a todo should not have to discover this by themselves.
Therefore, the todo app sends a reminder email to each user who has overdue todos.
### Steps
#### The reminder job sends the reminders
Every morning, the scheduler runs the reminder job. For each user who did not turn
off reminders in their settings, the job finds the overdue todos (in all todo lists
of the user), and sends one reminder that lists them. The reminder is stored, so that
a user gets at most one reminder per day, and is reminded of the same todo at most
three times.</br>
#### The user acts on the reminder
The user marks the todo as done, or gives it a new due date.
### Diagram
```mermaid
sequenceDiagram
participant Scheduler
participant SendReminders as SendReminders (action)
participant GetTodosToRemind as GetTodosToRemind (rule)
actor User
Scheduler->>SendReminders: send_reminders()
loop each user with reminders enabled
SendReminders->>GetTodosToRemind: get_todos_to_remind(user)
GetTodosToRemind-->>SendReminders: overdue todos
SendReminders->>User: reminder email
end
User->>User: mark todo as done, or change due date
```
"""[comment]: <> (This file is auto-generated. Do not edit directly.)
# s03_a_user_is_reminded_of_overdue_todos
## A user is reminded of their overdue todos
A user who forgets about a todo should not have to discover this by themselves.
Therefore, the todo app sends a reminder email to each user who has overdue todos.
### Steps
#### The reminder job sends the reminders
Every morning, the scheduler runs the reminder job. For each user who did not turn
off reminders in their settings, the job finds the overdue todos (in all todo lists
of the user), and sends one reminder that lists them. The reminder is stored, so that
a user gets at most one reminder per day, and is reminded of the same todo at most
three times.</br>
#### The user acts on the reminder
The user marks the todo as done, or gives it a new due date.
### Diagram
```mermaid
sequenceDiagram
...
```The pseudo-code in run_scenario refers to the code units of the application (e.g.
c.Reminders.Actions.SendReminders), so that the scenario also serves as a map of the code-base. But remember
that you can create your own scenario format if you don't like mine.
Once the scenarios are generated, we can delete the design doc (which should be empty) and test plan.
If the requirements change, then we repeat the entire process. We ask the AI to write and/or update workflow-tests, to update the scenarios, and finally to update the code so that all tests are green again. When an update is easy because it fits perfectly in the existing structure then you can ask the agent to fast-track the changes, which means that you review all the work (the design, the tests and the code) afterwards. This speeds up the process.