# Splitting the work into components the agent can build one at a time

> Split a feature into components with one responsibility and a one-sentence interface each, order them by dependency, and write the acceptance test for the first one before it exists, so it fails on a stub.

Page: https://schubergphilis.github.io/ai-training/coding-with-agents/decomposing-the-work/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

The to-do program gets a feature: "Let me import my old list. I have a
text file with one task per line, and I don't want to type forty `add`
commands." *Deciding whether to build it at all* taught how to write success criteria a stranger
could check, and this feature has them. In this lesson we take the feature apart before any
agent sees it. We split it into components with one responsibility each,
we draw the dependencies between them and put them in order, and we write
the check for the first component before it exists. Then we run that
check and watch it fail, which is the state the agent starts from.

An agent given a whole feature makes the split itself, and it makes a
different one every session. It also builds the parts together and
tests them at the end, so a mistake in the first part is hidden by the
parts built on it. Brilliant's skills map for coding with AI
lists decomposition, dependency analysis, and designing the verification as
skills of the engineer (Specification and design, Brilliant, Coding with AI skills map), and Claude Academy's playbook for
an AI-native development process writes the intent down in a file before
the code and keeps the checks running throughout (The AI-native SDLC playbook, Claude Academy). Both put the split in
your hands. The agent gets one component at a time, with its check.

## The fixture

The fixture is the to-do program from *Your first session with a coding
agent*, some weeks later. It has grown a `clear` command and been split
into three modules: `todo.py` holds the commands, `store.py` reads and
writes the list, and `render.py` formats it. It is in the course repository
under `site/examples/coding-with-agents/decomposing-the-work/fixture-repo/`.
Copy that directory to a place of its own and change into it. A copy has
no git history, so the reset is to delete the copy and copy again. Working
inside a clone of the course repository also works, and there
`git checkout -- .` is the reset, but the agent then reads the course's own
instruction files from the directories above the fixture. Claude Code, for
example, loads the instruction files in every directory above the one it
starts in (How Claude remembers your project, Claude Code documentation). The copy is the better default.

Run the program before you plan anything, so you know what the list looks
like now.

#### Example: Show the list

Run this, and compare what you see with the output below.

```sh
python3 todo.py list
```

Output:

```text
1. [ ] Buy milk
2. [x] Call the plumber
3. [x] Renew the passport
4. [ ] Water the plants
2 open, 2 done
```

The suite passes, and nothing in it changes until you add the first test of
this lesson.

#### Example: Run the tests

Run the tests and keep only the last line of the output.

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
OK
```

The examples in this lesson aren't graded. Each one is a command you run
in the fixture, with the output the course itself checks on every build.
The checkpoints ask about the split and the check, because making those is
what this lesson teaches.

## One responsibility per component

The success criteria for the import feature: `todo import tasks.txt` reads
a text file with one task per line. Spaces before and after a line are
dropped, a line that starts with `x ` is a done item, and blank lines are
skipped. A task already in the list isn't added twice, and the command
prints how many items it imported and how many it skipped. `add`, `list`,
`done` and `clear` keep working.

The request names one feature, and the work in it is three components. A component has one
responsibility, an interface you can state in a sentence, and a size that
fits one agent session and one change you can review in a few minutes. Cut
the feature where the data changes form: text lines become items, items
are merged into the list, and the list is shown to the user.

| Component | Responsibility | Interface |
| --------- | -------------- | --------- |
| Parser | Turns text lines into items | `importer.parse_lines(lines)` returns a list of items, each `{"text": ..., "done": ...}` |
| Merge | Adds new items to the list and leaves duplicates out | `importer.merge(items, new_items)` appends the items whose text isn't in `items` and returns the count added |
| Command | Wires `todo import <file>` to the two above and reports | `todo.py` reads the file, calls the parser and the merge, prints `imported N items, skipped M` |

Each row answers three questions with one sentence each. If a row needs
two sentences for its responsibility, it is two components. If the interface
sentence has to describe the code inside, the boundary is in the wrong
place. Trailing spaces and the `x ` mark are the parser's
problem and nobody else's, and "already in the list" is the merge's
problem, so the command has nothing to decide.

Write the list before the brief, and put it in the brief. The agent builds
the parser to the interface you named, and the brief for the merge names
the same interface, whether the same agent writes it or not.

#### Checkpoint: Which of these are components with one responsibility?

Which of these are components with one responsibility each?

Select exactly 2.

- A function that turns a list of text lines into a list of items, handling the done mark and the blank lines.
- A function that reads the file, parses the lines, skips duplicates and prints the report.
- A function that appends the items not yet in the list and returns how many it added.
- A function that parses the lines and also prints how many were blank.

## Order the work by dependency

Three components, and they can't be built in any order. The table fixes
the item format, so the merge could be written against it today. Its
check couldn't. The merge's test has to run on items the real parser
produced, or it proves the merge works on items you typed by hand and
says nothing about the imported ones. So the dependency runs from a
component to the checked output of the one before it. Draw the arrows.
The parser has no dependency. The merge depends on a checked parser, and
the command depends on both. That order is the plan, and each step is
checked before the next one starts.

The order also names the assumption everything rests on: the item format
the parser produces. The parser goes first and is checked alone with its
own test, and only then is the merge briefed. A component that passes its
own check is a fixed point the next brief can name.

The dependency drawing also shows which components have no arrow between
them. If the feature had a fourth component, a `render` change that shows
how many items came from an import, its arrow would point at the command
and not at the parser. A second agent session could build it once the
command was checked, while the merge was still in progress. Naming the
dependencies is what tells you what can run side by side and what has to
wait.

#### Pitfall: The merge inherited the parser's mistake

An engineer briefed the parser and the merge together. The agent built
both, tested the merge by feeding it items it had typed by hand, and
reported green. The parser kept the trailing spaces on each line, so an
imported `Buy milk ` and the existing `Buy milk` were two different texts,
and the merge added the duplicate. The merge's tests never saw a parsed
item, so they passed, and the bug showed up as a doubled list in the
command a week later, where nobody was looking at the parser. The rule:
check each component alone before the next one is built on it, and
feed the second component's test what the first one produces.

#### Checkpoint: Put the steps in dependency order

- Brief the command that reads the file and calls both
- Brief the merge and check its test on items the parser produced
- Brief the parser and check that its test passes
- Run the whole import against a copy of the list

## Design the check before the code

The parser doesn't exist yet, and this is the moment to decide how you
know it is right. Write the acceptance test now, put it in the suite,
and name it in the brief as the command that means done. The agent then
has a target it can run itself, and "done" means the line reads `OK`
instead of the agent saying so.

The test has to do one more thing: it has to fail on a stub. A parser that
returns an empty list, or that returns every line as an open item, is the
kind of first draft an agent writes when the target is vague. One input
with a blank line, a done mark and a line with spaces around it is enough
to reject each of those. Create `test_importer.py` with this content.

```python
import unittest

import importer


class ImporterTests(unittest.TestCase):
    def test_parse_lines_keeps_text_and_done_mark_and_skips_blank_lines(self):
        lines = ["Buy milk", "", "x Call the plumber", "  Water the plants  "]
        self.assertEqual(
            importer.parse_lines(lines),
            [
                {"text": "Buy milk", "done": False},
                {"text": "Call the plumber", "done": True},
                {"text": "Water the plants", "done": False},
            ],
        )
```

Run the suite. `importer.py` doesn't exist, so the test can't even import
it, and the suite reports an error.

#### Example: Run the suite before the parser exists

With `test_importer.py` in place, run the tests and keep the last line. The
course checks this against its own copy of the test, which is the one
shown above.

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
FAILED (errors=1)
```

Now prove the test does its job. Create `importer.py` with a stub that
has the right name and does nothing, and run the suite again.

```python
def parse_lines(lines):
    return []
```

#### Example: Run the suite against a stub

With the stub in place, run the tests and keep the last line. The course
checks this against its own copy of the stub.

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
FAILED (failures=1)
```

An error became a failure: the module imports, and the assertion rejects
what it returns. Delete `importer.py` again. The agent writes the real one. Your
brief for the first component now fits in a few lines: the parser's
responsibility and interface from the table, the file it may create, and
the command that means done.

```text
Add importer.py with parse_lines(lines), which turns text lines into
items as test_importer.py describes. Create only importer.py. Done when
`python3 -m unittest -q` prints OK. Do not change the test.
```

An agent that can't make the test pass may change
the test until it does, and then the check doesn't check anything. The
last line of the brief closes that door.

#### Checkpoint: Which test would fail on a stub?

Which of these tests would fail on a stub parser?

- Assert that `parse_lines` on the same four lines returns a list of dictionaries, each with a `text` and a `done` key.
- Assert that `parse_lines` on the same four lines runs to the end without raising an error.
- Assert that `parse_lines` on the same four lines returns three items, with the texts and done marks the lines describe.
- Assert that `importer.py` exists next to `todo.py` and defines a function called `parse_lines` that takes one argument.

## Exercise

Reset the fixture and take the import feature apart yourself, without
looking at the table above. Write the components as a list with one
responsibility and one interface sentence each, draw the dependency
arrows between them, and put them in build order. Then write the failing
acceptance test for the first component, run
`python3 -m unittest -q`, and check that the last line starts with
`FAILED`. Add a stub with the right name and run it again, and the line
must still start with `FAILED`. Fifteen lines of notes and one test file is
enough. You do this so that the agent builds one checked component at a
time and never gets to choose the split.

A good result: each component's interface fits in a sentence, the first
one in your order is the one the others take input from, the suite says
`FAILED` before the component exists and again on the stub, and the brief
names the test run as what done means. Reflect: which of your components
would you have merged if the agent had been given the whole feature, and
what mistake could then have hidden inside it?

Stretch: Brief the agent for the parser with the brief above, in a fresh session, and read the diff: it should touch `importer.py` and nothing else.

## Recap

1. A component has one responsibility, an interface that fits in a sentence, and a size that fits one agent session and one reviewable change. Cut where the data changes form.
2. Dependencies set the build order. The component whose output the others take is built and checked first, so a mistake in it never reaches the next one unseen.
3. A component checked alone is a fixed point for the next brief. Feed the next component's test what the first one produces, or the second inherits and hides the first one's mistakes.
4. Write the acceptance test before the component exists, put it in the suite and name the test run in the brief as what done means.
5. A check that passes on a stub doesn't check the work. Run it against a version with the right name and no work, and the line must still say it failed.

## References

- Specification and design, Brilliant, Coding with AI skills map
- The AI-native SDLC playbook, Claude Academy, https://academy.claude.com/courses/ai-native-sdlc-playbook
- How Claude remembers your project, Claude Code documentation, https://code.claude.com/docs/en/memory
