Chapter 02 / 07
Diagnose
Explain how the first F2 rule produces a signal and what that observation leaves unchecked.
What you should leave with
- Shared intuition
- we can explain the match by pointing to log dot in the added line.
- Shared language
- F2 is the category, the YAML supplies the rule, and the recorded observation is a signal. A leak is a claim we'd need to check against what actually gets logged.
- Shared behavior
- open the named code and follow the data before reporting a defect.
What we are checking
Original classroom timing
| Minutes | What happens |
|---|---|
| 8–10 | Why Diagnose helps, and what a task record is |
| 10–12 | Introduce the first F2 rule and run the command |
| 12–15 | Follow the task record into the branch diff |
| 15–19 | Follow the added logging line through the matcher |
| 19–21 | Read the signal and its limits |
| 21–23 | Explain the observation and carry the question into Review |
The agent was asked to add refund retries, but it also changed logging and fees.
Diagnose gives us a way to pick out changes that deserve a closer look.
It searches the diff using rules written for this project.
We’ll follow one of those rules to the logging line, then look at how SPEED records the
match.
We first need to tell SPEED which work we’re checking.
It stores each task in a JSON file called a task record.
This one belongs to the payments feature and has the ID 1.
It names the round-0 branch, records the agent model, and lists the four files from the
opening.
We’ll use its branch to find the changes.
The task ID identifies this record; it isn’t a commit number.
Source references
Fixture: .speed/features/payments/tasks/1.json, lines 2–11.
Point to id, branch, agent_model, and files_touched.
Read the F2 rule
The project also needs to tell Diagnose what to look for.
In speed.toml, classes_file points to the YAML file beside it.
F2 is the cardholder-data leakage category in that file.
It has two rules, and we’re going to read the first one.
The second stays enabled, so it can still appear in the output.
Source references
Fixture: speed.toml, lines 8–10, especially classes_file at line 9;
.speed/classes.yaml, lines 17–22.
Keep the second rule at lines 23–25 folded.
- id: F2
title: Cardholder data leakage
rules:
- look: added-lines
match: '\b(log|logger|webhook|telemetry)\s*\.'
say: "{n} file(s) with an observability sink call in the added lines"Read the three fields together.
The look field selects added lines, match gives us the pattern, and say gives us the
message to write when something matches.
Here the pattern looks for log, logger, webhook, or telemetry at a word boundary.
After that name, it allows whitespace and requires a dot.
So log dot error fits the pattern.
The name “Cardholder data leakage” tells us why the project includes this rule.
The pattern itself only looks for those names and a dot; it doesn’t check the arguments
or even require a function call.
Run Diagnose
When a rule matches, Diagnose records an observation called a signal.
It writes these signals into a file called the risk surface, under the project’s failure
categories.
That file also has fields for judgments we’ll make later.
Let’s run Diagnose for the payments task we just opened.
speed diagnose --feature payments --task 1The console identifies the comparison as main to round-0 and reports two signals under
F2.
Each of F2’s rules found something, which accounts for the two entries.
We’ll inspect the first entry.
To explain where it came from, start with the command that read our task record.
Source references
SPEED: lib/cmd/diagnose.sh, lines 109–116, counts signal entries for the console.
Fixture: saved .speed/features/payments/risk-surface.yaml, lines 16–28, contains both
F2 signals.
Follow the implementation
The shell joins the configured rules path to the fixture root, then reads Task 1’s JSON.
It takes the branch, agent model, and declared files from that record.
The comment calls out three fields it leaves behind: description, acceptance criteria,
and review feedback.
Those fields describe or judge the work; the comment says Diagnose mustn’t take its
verdict from them.
The shell then writes the branch diff into a temporary file for the Python engine to
read.
Source references
SPEED: lib/cmd/diagnose.sh, lines 32–54;
lib/tasks.sh, lines 66–76, reads the task JSON;
lib/cmd/diagnose.sh, lines 61–81, especially line 74, creates the diff file;
lib/git.sh, lines 326–330, runs the three-dot comparison.
The Git helper uses a three-dot comparison.
Here that means changes on round-0 since its common ancestor with main.
The opening showed one commit, but this comparison includes the course setup as well.
The task’s four declared files don’t restrict this diff.
This command shows just the service’s portion so we can find the logging change.
git diff main...round-0 -- src/payments/service.tslog.error(serialiseFailure(error, req));Source references
Fixture: src/payments/service.ts, line 67, is the added logging call.
Git shows this replacement as a whole added line, even though most of the call stayed
the same.
That’s why an added-lines rule will examine it.
The shell sends the diff and rules to Python, along with the declared files and any
configured spec path.
Those last two inputs serve other rules; this rule searches the added text.
Source references
SPEED: lib/cmd/diagnose.sh, lines 93–100, invokes lib/diagnose_engine.py;
lib/diagnose_engine.py, lines 598–605, loads the rules and prepares the inputs.
The Python code walks through the diff, remembering the filename from each file header.
The hunk header supplies the line position.
When it reaches an added line, it removes the leading plus sign and keeps the filename,
position, and text together.
For our example, that’s the service file, line 67, and the logging call.
Headers, removed lines, and unchanged lines aren’t entries in this list.
No TypeScript parsing is involved.
Source references
SPEED: lib/diagnose_engine.py, lines 315–347, extracts the records.
This replaces the older script’s reference to the previous extraction helper.
The rule’s look field sends that list to the added-lines matcher.
Look at pattern.search: it searches each line using the regular expression from our YAML
file.
On the logging line it finds log dot, so count increases by one.
The matcher keeps the filename once and records the matching line and snippet as
evidence.
That distinction matters if two lines in one file match: the count is two, but the file
list still has one entry.
Several matches on the same line also increase the count only once.
Source references
SPEED: lib/diagnose_engine.py, lines 609–612, runs the rules; lines 575–579 select the
matcher; lines 411–432 implement it.
Now read the rule’s message beside that count.
It says “files,” although the code counts matching lines.
We happen to have one line in one file, so the number looks right here.
The engine substitutes that count for the braces in the message and makes one signal for
the rule.
It includes the file list and evidence, then the shell writes them to the risk surface.
Source references
SPEED: lib/diagnose_engine.py, lines 591–618, formats and assembles the signal;
lib/cmd/diagnose.sh, lines 135–139 and 158–199, writes the output.
Interpret the signal
- id: F2
failureMode: "Cardholder data leakage"
plausible: undecided
costOfMissing: undecided
findableByReading: undecided
signals:
- observed: "1 file(s) with an observability sink call in the added lines"
where:
- "src/payments/service.ts"Source references
Fixture: saved .speed/features/payments/risk-surface.yaml, line 2, identifies Task 1.
Lines 16–24 contain this excerpt; it omits the second signal.
The saved file predates the current writer’s detailed evidence fields.
After a fresh run, verify the output and its line numbers again.
There’s the message we just built, with the service path below it.
The three judgment fields remain undecided.
They ask whether the failure is plausible, what missing it would cost, and whether
reading could find it.
The writer leaves them open because the match hasn’t answered those questions.
It hasn’t followed the request into serialiseFailure or checked what the logger
receives.
A redacted logging call would match too, while a logging name outside this pattern could
be missed.
I would describe this result like this.
“The first F2 rule matched the added log.error line in the payments service.
We need to follow serialiseFailure to see what reaches the log.”
That gives another engineer the location and the question to check.
We’ll keep both beside us when we run Review; its input doesn’t include this
risk-surface file.
Shared intuition: we can explain the match by pointing to log dot in the added line.
Shared language: F2 is the category, the YAML supplies the rule, and the recorded
observation is a signal.
A leak is a claim we’d need to check against what actually gets logged.
Shared behavior: open the named code and follow the data before reporting a defect.
Preparation and evidence notes
Presenter notes: Stay with Task 1 and F2’s first rule.
The audience has seen the business request, four files, and spec paths, but hasn’t
learned SPEED vocabulary before this section.
Use selected shell and Python blocks; the other matchers, YAML parser, and evidence
storage aren’t part of the close reading.
Task 1’s title is a regression-fixture label, and its nested workbench_source_task is
retained metadata from other work.
Neither supplies the payments story or this rule’s input.
Those fields are in fixture .speed/features/payments/tasks/1.json, lines 3 and 12–48.
Preparation and evidence notes
Presenter notes: Preserve the broad branch diff and the rule’s misleading “files” label.
Leave judgments open despite the console instruction to fill verdicts before running
checks.
That instruction is in SPEED lib/cmd/diagnose.sh, line 143.
Save the serializer walkthrough for Review; don’t stage a leak during Diagnose.
The original script records a Homebrew Bash Diagnose run on 2026-09-29.
This port inspected the saved output and current code without regenerating it.
The script has been reviewed as text, not rehearsed aloud.
Carry forward
The first F2 rule matched the added log.error line. Carry the question of what serialiseFailure sends to the log into Review.
Help me reason through this
Find log dot in the added line. This is a text match; the matcher has not traced the arguments through the serializer.
Your explanation is saved in this browser.