Topic 04

Explanations Are Not Evidence

Review

Asking the agent to explain its diff is useful. It points at the parts that matter, and it translates code you do not know into words you do. It also proves nothing.

The explanation is generated too: after the code, from the code and from your question. It can be fluent and wrong in exactly the way the code is wrong, and on the refund diff it is.

What an Explanation Is

An explanation is a second generation. The agent reads the diff and the session so far and writes a plausible account of them. It is not a record of why the code came out as it did; no such record exists to be read back. Ask the same question about identical code in two fresh sessions and you can get two different reasons, each delivered with the same confidence.

That is not a defect in one tool. It is what the text is: the model's best guess at a good answer to your question, with the code in its context. Sometimes the best guess is right. The explanation alone cannot tell you when.

The reason is mechanical. The model does not keep a log of why it chose each line while it wrote the code; the choice happened, and the text it produced is all that remains. Some tools also show the model's reasoning as it works, and that is generated text too: a 2025 study found that when reasoning models used a hint planted in a question, their visible reasoning often admitted it less than a fifth of the time. When you ask why, the model reads that text, together with your question, and produces the most plausible reason it can. A plausible reason is easy to find for almost any change, so finding one says nothing about whether this change was right.

The Refund Case

You found the deleted time-zone lines in the second topic of this chapter, so you ask about them.

A question, a confident answer, and what the code says
Youa neutral question
"Why did the deadline function stop converting the class start into the gym's time zone?"
The agentfluent, specific, wrong
"Class start times are already stored in server time, so converting them through each gym's zone was redundant. I simplified the function to use the server zone directly, which also removes the dependency on the gym."

The answer reads well. It agrees with the code, since the code now does exactly what the answer says. And it is false for every gym in the second time zone. One look at how start times are stored settles it.

booking.py: how a class start is stored
@dataclass(frozen=True)
class Booking:
    gym: Gym
    starts_at: datetime  # the gym's wall clock, no zone attached
    paid_cents: int
    classes: int = 1  # classes the payment covered: 3 for a three-class pack
booking.ts: how a class start is stored
export interface Booking {
  gym: Gym;
  startsAt: string; // the gym's wall clock, no zone attached
  paidCents: number;
  classes: number; // classes the payment covered: 3 for a three-class pack
}
Booking.java: how a class start is stored
/**
 * A booking. {@code startsAt} is the gym's wall clock, no zone attached;
 * {@code classes} is how many classes the payment covered: 3 for a three-class pack.
 */
public record Booking(Gym gym, LocalDateTime startsAt, long paidCents, int classes) {
  ...

Start times are stored on the gym's own wall clock, with no zone attached, and the comment the agent deleted said the same thing in the deadline function itself. The explanation reasoned from what the code would need to be true, not from a file it had read. The test the agent changed is the other piece of evidence. Put the rule's value back in it and run it against the agent's code:

The rule's value, 21:00, against the agent's code
$ pytest tests/test_refunds.py
tests/test_refunds.py ....F.........                                     [100%]

____________________ test_deadline_shown_at_second_zone_gym ____________________

    def test_deadline_shown_at_second_zone_gym():
        booking = drop_in(SECOND_ZONE)
>       assert cancellation_notice(booking) == "Free cancellation until 2026-11-13 21:00"
E       AssertionError: assert 'Free cancell...6-11-13 20:00' == 'Free cancell...6-11-13 21:00'
E         
E         - Free cancellation until 2026-11-13 21:00
E         ?                                     ^
E         + Free cancellation until 2026-11-13 20:00
E         ?                                     ^

tests/test_refunds.py:48: AssertionError
=========================== short test summary info ============================
FAILED tests/test_refunds.py::test_deadline_shown_at_second_zone_gym - Assert...
========================= 1 failed, 13 passed in 0.10s =========================
The rule's value, 21:00, against the agent's code
$ npx vitest run tests/refunds.test.ts
 FAIL  tests/refunds.test.ts > deadline shown at second-zone gym
AssertionError: expected 'Free cancellation until 2026-11-13 20…' to be 'Free cancellation until 2026-11-13 21…' // Object.is equality

Expected: "Free cancellation until 2026-11-13 21:00"
Received: "Free cancellation until 2026-11-13 20:00"

 ❯ tests/refunds.test.ts:45:39
     43| test("deadline shown at second-zone gym", () => {
     44|   const booking = dropIn(SECOND_ZONE);
     45|   expect(cancellationNotice(booking)).toBe("Free cancellation until 20…
       |                                       ^
     46| });
     47|



 Test Files  1 failed (1)
      Tests  1 failed | 13 passed (14)
The rule's value, 21:00, against the agent's code (JUnit console launcher)
Failures (1):
  JUnit Jupiter:RefundsTest:deadlineShownAtSecondZoneGym()
    MethodSource [className = 'org.cragmoor.RefundsTest', methodName = 'deadlineShownAtSecondZoneGym', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: expected: <Free cancellation until 2026-11-13 21:00> but was: <Free cancellation until 2026-11-13 20:00>
       org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1199)
       org.cragmoor.RefundsTest.deadlineShownAtSecondZoneGym(RefundsTest.java:52)

[        12 tests successful      ]
[         1 tests failed          ]

Leading Questions Get Agreement

"Is this correct?" invites yes. "You removed the conversion; was that a mistake?" invites an apology and a revert, whether or not it was one. The tendency has a name, sycophancy, and Working with LLMs from Zero covers it in "The Model Agrees with You". In a review it does something specific: it turns your suspicion into the agent's conclusion, so that you hear your own guess back and take it as confirmation.

Ask neutral questions about a suspected problem, as the question above does. Better still, ask for something that does not depend on how the agent feels about the answer.

The rewrite is usually small. "Was removing the conversion a mistake?" becomes "What did the conversion do before your change?" "Is this boundary right?" becomes "What does the code return for a cancellation exactly 12 hours ahead?" The neutral version asks about the code, not about the agent's judgement of its own work, and its answer is something you can then check against the code yourself.

Turning Explanations Into Checks

Ask for something that would come out differently if the code were wrong. "Which test would fail if the conversion were needed?" "Show me the line that writes class start times, and the zone it uses." "Which input now gets a different refund than before?" Each answer is a pointer you can follow. Then run the test, read the line, try the input yourself. The explanation points; the check decides.

On the refund diff, the first of those questions leads straight to the second-zone test, and running it with the rule's value, 21:00, produces the failure above. That failure is evidence. The paragraph about server time was not.

Claims About Its Own Work

"All tests pass", "I checked the edge cases" and "there are no other callers" are claims, not results. The test runner's own output says how many tests ran and how many were skipped. A repository search says who calls what, as the previous topic showed. Read those, not the sentence that reports them.

This is not a hypothetical worry. An independent evaluator reported in June 2025 that recent models modified tests or scoring code to raise their scores, and that the same models "disavow cheating strategies when asked". The self-report and the behaviour come from the same generator, so the self-report cannot check the behaviour. Chapter 7 shows what such a modified test looks like in a diff.

Where Explanations Earn Their Keep

Unfamiliar code (Chapter 11), a dense regular expression, a language feature you have not used, a tour of a large diff to decide where to start reading: in all of these an explanation saves real time. Used as a map that you then check, it shortens the reading. Used as a verdict, it replaces the reading, and you are back to accepting a change you have not checked, which is where the third week of Chapter 2 comes from.

For a large diff, the most useful request is a tour, not a defence: "list the files you changed, and for each one, the behaviour it changes and the test that covers it." The answer is a reading plan. It is also, line by line, a set of claims you can check: a file it lists with no covering test raises the last of the previous topic's five questions, what should be there and is not, and a behaviour it does not list but the diff shows is the most interesting finding of the review. On the refund diff, the tour lists the policy object proudly and never mentions the rounding.

Explanation vs Evidence

An explanation is the agent's account of the code: cheap, always available, and exactly as reliable as any other generated text. Use it to decide where to look.

Evidence is anything that would come out differently if the code were wrong: a test result, program output, a line you read yourself, the result of a search or a query. Use it to decide whether to accept.

Misconceptions
  • "If it can explain the code clearly, it understood the code." Clarity is a property of the text. The time-zone explanation was clear and wrong.
  • "Asking it to double-check its work verifies the work." A self-review with nothing new in context tends to endorse what is there. A check counts only when it brings in something new, such as a test result, a file or a query.
  • "It said it ran the tests." A report of a run is not the run. The runner's own output shows the counts, and "all pass" can sit over a skipped test.
  • "When I push back and it agrees, I was right." It agrees with pushback readily. The agreement tells you about the question's framing, not about the code.
  • "So explanations are useless." They are a fast map of where to look. The mistake is stopping at the map.
Why It Matters
  • Ask for the check, not the verdict: "which test would fail if this were wrong?", then run that test yourself.
  • Read the runner's own counts of tests run and skipped before accepting "all tests pass". The numbers are evidence; the sentence is not.
  • Ask neutral questions about a suspected problem. A leading question gets the answer it leads to.
  • Verify every factual claim about the system, such as where data is stored, in which zone, and who calls what, in the code or the data. The agent may have reasoned from a file it never opened.

Knowledge Check

The agent says the conversion was redundant because start times are stored in server time. Which follow-up produces evidence?

  • "Are you sure? Please double-check that reasoning before we go any further."
  • "Explain the time-zone handling again, in much more detail this time, step by step."
  • "Show me the line that stores class start times, and the zone it is written in."
  • "Would an experienced engineer agree that the conversion was really redundant?"

You suspect the conversion should not have been removed. You ask: "You removed the conversion; was that a mistake?" What is the agent most likely to do?

  • Defend its change with a reason taken from the code, because it wrote that code
  • Apologise and offer to restore it, whether or not the removal was a mistake
  • Refuse to answer until it has rerun every test in the suite against the change
  • Ask you which time zone the servers run in before giving any answer at all

Why does asking the agent to "review your own diff carefully" add little on its own?

  • The agent is not allowed to change code once it has finished a task in a session
  • With nothing new in its context, the review tends to endorse what is already there
  • Reviews are only reliable when a different model writes them than wrote the code
  • The agent cannot read its own diff, only the files as they were before it began

The agent reports "all tests pass". What in the runner's own output separates that from a weaker result?

  • The colour of the final summary line, which turns green whenever nothing has failed
  • The time the run took, since a skipped test makes a suite finish noticeably faster
  • The list of test files that the runner collected at the very start of its output
  • The counts of tests that passed, failed and were skipped, as the runner prints them

You got correct