todo-app/docs/DOCS-llm-design-spec-audit.md
Krzysztof kuhy Rudnicki a0d37c4df8
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Re-point paths for the ~ reorganisation (home-tidy)
Also bumps Flutter 3.47.3, flutter_secure_storage 11.1.0, idb_shim 2.9.9,
sqflite_common_ffi 2.4.3, very_good_analysis 11 and mcp 2.2.0 so the
freshness gate lets this through; v11 adds async_return_with_no_await, so
the 18 flagged return sites now await explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 19:49:03 +02:00

219 lines
13 KiB
Markdown

# Audit of the `llm-design-spec` note template against 514 real Claude sessions
## Context
`~/src/todo`'s default note template (`lib/data/note_template.dart`, `NoteTemplate.llmDesignSpec`)
has 12 sections: title, what, where, tech, must, ask, nice, never, done, depends,
estimate, refs. It was designed top-down from the `<work_backlog>` format in
`~/.claude/CLAUDE.md`, never validated against what actually gets written or what
Claude actually reads.
This audit measures it against the real corpus. Everything below is measured, not
estimated. Fill rate alone was deliberately **not** used as the verdict — four
sections say "Leave blank if none" in their own helper, so a low fill rate is the
field working correctly. The discriminating test is outcome-linked: *was the section
read, and did leaving it out cause a failure?*
## Evidence base
| Source | Size |
| --- | --- |
| `~/.claude/projects/**/*.jsonl` | 514 sessions, 495 with real user prompts, **1503 prompts** |
| `~/src/todo/BACKLOG.md` | 49 notes — **25 structured, 24 freeform** |
| note → session join (title match) | **22 note/session pairs**, 21 of 25 notes traced |
| `~/.claude/memories/mistakes.md` + `projects/-home-kuhy/memory/feedback-*.md` | 13 dated mistakes, 33 feedback memories |
**How you actually prompt** (this frames everything else):
- median prompt **324 chars**; 44% under 200 chars; 26% under 100
- only **21 of 495 sessions (4%)** open with a structured template note
- of the 24 freeform notes, **9 start with a bare URL and 8 are *only* a URL**
Half the backlog bypasses the template entirely. Any recommendation below is
net-subtractive by default; every proposed addition has a named failure behind it.
## What Claude did not use (deterministic tests over the 22 pairs)
| Section | Filled | Provably used | Test |
| --- | ---: | ---: | --- |
| `estimate` | 22 | **1 (4%)** | word "estimate" ever appears in Claude's output |
| `done` | 22 | **4 (18%)** | the done text is quoted back at verification time |
| `refs` | 14 | **5 (35%)** | any listed URL/domain hit by WebFetch/servo-fetch/WebSearch/curl |
| `tech` | 22 | 20 (90%) echoed — but Claude opened `pubspec.yaml`/`pyproject.toml`/`CLAUDE.md` **anyway in 54%** | token overlap + manifest reads |
| `ask` | 9 | 100% asked — **but 92% asked without it too** (median 4 asks vs 5) | AskUserQuestion / ExitPlanMode calls |
The `ask` row is the sharpest result: filling it changes Claude's asking behaviour by
roughly nothing, because `~/.claude/CLAUDE.md`'s "Ask first when unsure" + the
spec-first gate already force it. The section is inert.
`done` at 18% is the most damaging: you write an observable success condition and in
four cases out of five Claude verified against a criterion it invented itself.
## Where Claude went wrong, and what would have prevented it
Hand-read of the corrections inside note-driven sessions plus the sampled corrections
across all 495 sessions. Three recurring classes, all traceable to a template gap:
1. **"Test it yourself, on the device"** — recurs verbatim across wake_alarm,
diet_guard, screen-locker: *"as I said before, test it yourself pleeease"*,
*"You did not install this new vesrion on phone, do not bother testing on desktop"*,
*"isntead of making me test it test it yourself"*. Backed by five separate feedback
memories (`feedback-device-verification-before-tests`,
`feedback-verify-real-deployment-path`, `feedback-stop-when-device-disconnected`,
`feedback-blind-adb-tap-unreliable`, `feedback-flutter-install-uninstalls`).
**Gap: there is no `verify` section.** `done` conflates *what success looks like*
with *how and where to check it*, and Claude drops the second half.
Confirming evidence: 3 of the 7 filled `never` sections are smuggling this in —
*"test ONLY on the phone, if the phone is NOT connected … STOP AND LET ME KNOW"*,
*"do NOT work if the phone is NOT connected"*.
2. **Wrong *kind* of fix** — *"you are fixing the problem in a wrong way, instead of
devising an algorithm to detect if a game is winnable 100% you are playing around
[with the] number of pieces"*, *"I said make it possible to pass but you made it
wayyy to easy"*. **Gap: `done` accepts a vague sentence.** This exact failure had
to be patched globally afterwards ("Algorithmic guarantees over parameter tuning"
in `workflow-rules.md`) — a note-level threshold would have caught it first.
3. **UI intent mismatch** — *"I was thinking more of an app icon like the one for
network or nvidia"*, *"both sliders have bad ux"*, *"why is it upside down"*,
*"this is ugly as it is not symmetrical"*. **Gap: nowhere to attach a visual
reference.** `refs` is worded as reading material and is ignored 65% of the time.
This is the same failure as the 2026-07-21 mistake entry (reference image read as
content instead of format).
**Negative result worth stating plainly:** the other 10 of 13 entries in
`mistakes.md` — git stash, feature-branch-vs-main, killing a running game session,
mid-queue status reports, conftest-before-tests, heredoc escaping, over-claiming a
rollout complete — map to **no template section at all**. They are global process
failures. This is the argument against growing the template to chase them: it would
add fields that cannot work.
## Per-section verdict
| Section | Verdict | Grounds |
| --- | --- | --- |
| `title` | **keep** | 25/25 |
| `what` | **keep** | 25/25 — the load-bearing section |
| `where` | **keep, widen** | 25/25. Widen to allow "the fix belongs in another repo" — two corrections were exactly that (*"our INSTALLER, installing files in a WRONG FOLDER"*, *"The real fix is in ~/src/screen-locker (a different repo)"*) |
| `tech` | **merge into `where`** | Claude reads the manifest anyway in 54% of sessions. Load-bearing only for `where — new app: <name>`, which is where it should live |
| `must` | **keep** | 25/25 |
| `ask` | **remove** | Provably inert: 92% ask rate without it vs 100% with it. Global rules already own this |
| `nice` | **remove** | 13/25, no evidence of use, and it invites the gold-plating that `never` then has to forbid |
| `never` | **remove as a section** | 2 of 7 literally say "none"; 3 of the rest are verification constraints (→ `verify`); 1 is a `where` constraint; 1 is genuine scope (*"do not touch any other pieces of Todo app"*) → fold that shape into `must` as a "must not" line |
| `done` | **keep, tighten** | 25/25 filled but quoted back only 18%. Helper must demand a threshold or a check command, not a sentence |
| `depends` | **remove** | 13/25, zero evidence of use downstream |
| `estimate` | **remove** | 25/25 filled, referenced once in 22 sessions. Pure write-only metadata |
| `refs` | **keep, rename → `read first`** | Fetched in only 35% of cases. The name reads as "citations"; the helper already says "read first" — make the name match the instruction |
| — | **add `verify`** | See failure class 1. Where and how to check: device, command, deploy path |
| — | **add a visual-reference slot** | See failure class 3 — either a `looks like` section or image support in `read first` |
## What was implemented (2026-07-26)
**12 sections → 7:** `title, what, where, must, done, verify, read first` —
`never` and `nice` became `must not:` / `optional:` line prefixes inside `must`
rather than sections of their own, and `tech` was folded into `where`.
Landed in:
- `lib/data/note_template.dart` — the new section list, plus
`NoteTemplate.retiredLabels`. The audit turned up a latent bug while testing
this: `parse()` treats an unknown `## heading` as content (deliberately — users
write subheadings inside values), which meant a legacy `## tech` block was
silently folded into the preceding section's value **and the note still
reported as conforming**. Opening any old note in the stepper would have
swallowed it on the next save. Retired labels are now explicitly
non-conforming, so such notes open in the raw editor untouched.
- `tool/migrate_backlog.dart` — export → transform → import migration, with two
gates that throw rather than warn: every rewritten note must parse as
conforming, and no source line may vanish except the deliberately dropped
`ask`/`depends`/`estimate`.
- `CLAUDE.md` (this repo, which was documenting a dead `out` section) and
`~/.claude/CLAUDE.md`'s `<work_backlog>` format.
## Part 2 — what the opening prompt leaves out, and why more fields won't fix it
The first half of this audit asked "which template sections get read". The more
useful question is "what does the opening prompt *miss*, and what would stop it
being missed". Measured over 246 sessions with a real top-level prompt (sidechain
and subagent turns excluded — they inflate every count roughly 2x).
**Three results, and the third overturns the premise.**
1. **Claude has to ask in 52% of sessions** (129/246), 716 questions total.
2. **The first question arrives at assistant turn 24 (median); only 3% of asking
sessions ask within the first three turns.** So Claude is almost never asking
because the prompt was thin. It asks after ~24 turns of reading code and
hitting a fork that *did not exist* at prompt time. These are discoveries, not
omissions.
3. **Opening-prompt length does not predict corrections.** Split 123 real
multi-prompt sessions into thirds by opening length:
| opening length | n | median | corrections/session |
| --- | ---: | ---: | ---: |
| shortest third | 41 | 102 ch | 0.29 |
| middle third | 41 | 410 ch | 0.41 |
| longest third | 41 | 2133 ch | 0.22 |
A 20x longer opening buys a 0.07 reduction, and the middle third is the worst.
**Writing more upfront does not measurably help.** Any advice of the form
"fill in more of the template before you start" is unsupported by the corpus.
### What genuinely does arrive late
62% of multi-prompt sessions get a constraint after prompt 1; 56% of opening
prompts carry no constraint at all. Of 152 late-constraint messages:
| what the late constraint is about | n |
| --- | ---: |
| scope limit ("only this", "don't touch X") | 30 |
| behaviour rule ("should always/never …") | 18 |
| device / where to test | 17 |
| visual-UX rule | 17 |
| process/git | 10 |
Only the middle-of-the-road categories are knowable at prompt time. **"Device /
where to test" is the one that is knowable, constant, and still repeated** — for
`todo`, `diet_guard_app`, `workout_app` and `wake_alarm` the answer is always the
phone, and it was still typed out 17 times, plus 5 feedback memories, plus 3 of
the 7 old `never` sections.
### The uncomfortable conclusion
That constant already has a standing rule, a memory (`prefer-mobile-testing`) and
a whole skill (`phone-deploy`) telling the agent mobile is primary — and it was
still violated 17 times. **A constraint that is already written down three times
and still missed is not a prompting defect.** No extra field, longer note, or
stricter capture discipline can fix it, which is why this audit adds `verify` as
the *only* new section and stops there.
The leverage is therefore not at capture time:
- **Capture stays cheap.** The evidence supports fewer fields, not more. Median
opening prompt is 363 chars and that is fine. Implemented: a draft that is
nothing but a pasted link now selects the freeform `blank` template
automatically (`NoteTemplate.forDraft`), so filing a link costs one paste
instead of a seven-step stepper — a third of the freeform backlog is exactly
that. Typing any prose alongside the link restores the spec template, and an
explicit pick from the dropdown or wizard is never overridden.
- **Per-repo constants belong in that repo's `CLAUDE.md`, written once** — never
re-typed per note. `verify` exists for the exception, not the rule.
- **The real cost centre is the turn-24 fork**, which no prompt could have
pre-empted. Improving that means better mid-session surfacing of decisions, not
a better opening prompt.
## Re-running this audit
The measurements are all reproducible from `~/.claude/projects/**/*.jsonl` plus a
fresh `BACKLOG.md` export. The two that matter for judging a section's worth:
- **is it read?** — join a note to the sessions that consumed it (match on the
note's title), then test the section against the transcript: were the `refs`
URLs actually fetched, was `done` quoted back at verification time, does
`estimate` appear anywhere downstream.
- **does it change behaviour?** — compare sessions where the section was filled
against sessions where it was blank. This is what condemned `ask`: a 100% ask
rate with it versus 92% without is not an effect.
Fill rate alone proves nothing — four of the removed sections said "Leave blank
if none" in their own helper text, so a low fill rate was them working correctly.
Do not re-add a section on a coverage argument; re-add it when a failure names
it.