Codebook × LLM — the state engine

Two state axes, the combinations a researcher can reach, what each one renders, and — from §6 — what was decided about them. Every claim about current behaviour was read out of the code at HEAD on 3 Sep 2026 and carries a file:line; the decisions carry no receipt because nothing is built yet. §7 is what building them costs, and §8 is what is still open.


1  Axis A — LLM availability (global)

Four are configuration and never stale; one is a cached verdict about someone else’s server. The transient conditions at the bottom are not states — nothing persists them, and the SDK rides most of them out before anyone sees one.

StateEstablished by Surfaced asNote
Ready Key present for the current providerroutes/autocode.py:146 _has_api_key — (nothing to say) The overwhelmingly common case.
No key No key for the resolved provider Refusal toastrefusal.py NO_API_KEY → autocodeRefusal.ts Pre-flight, so it costs nothing to discover.wired
Ambiguous 2+ keys, none chosenconfig.py:305 provider_resolution_for Refusal toast A server cannot prompt, so it refuses rather than picking a vendor.
Local only llm_provider == "local" Refusal toast Ollama cannot hold the taxonomy in context.
Out of credit Sticky verdict from a real callLLMValidator / OutOfCreditModel Titlebar pill + popoverout-of-credit-ux.html §2 Only isolable on Anthropic + OpenAI. Azure has no per-account credit; Gemini folds it into one exhausted signal.
rate-limited
server error
network
Transient; SDK retries 6× honouring Retry-Afterllm/client.py:155 _CLOUD_MAX_RETRIES Only as a job failure, if retries are exhausted Not states. Nothing persists them, and there is deliberately no hand-rolled backoff — the SDK’s is better and sees the header.

2  Axis B — codebook state (per framework)

Decided: a codebook is installed or not installed, and that is very nearly all of it. A run is a per-project activity, not a property of the codebook — so it belongs where per-project activity already lives, and the codebook card carries at most an acknowledgement that the click landed.

The codebase already states this rule, in the copying case of ProjectSubtitle: the toolbar copy pill was removed because “copy is a per-project op, so it lives on the row; the title-bar pill is reserved for app-global ops… Mac direct manipulation: feedback appears on the row you dropped onto.” AutoCode is the same shape. Out-of-credit, being genuinely app-global, is the pill’s business and nobody else’s.

Sub-axisValuesWhere it belongs
Installedno / yes the card — Install vs UninstallCodebookV2.tsx:266
Enabledon / off the card — switch + pageoff washCodebookV2Sidebar.tsx:127
Undecided proposals0 / n the card — “· n undecided” + Review doorCodebookV2Page.tsx:195
Run in flightpending · running Project row + the chip. The card echoes it as ✦ Autocoding… and no more — the acknowledgement that the click landed, on the thing that was clicked.
Run outcomefailed · cancelled The project status popover — its stated job is “telling an alpha tester what failed”.
Coverage shortfallcompleted, processed < total Project row + its diagnostic popover. Not the card, not the details page. An incomplete run is an event that belongs to the project, and the popover already exists to help diagnose one.

Where the split holds

All the way. A run in flight has the project row and the chip, and the card echoes only that it started. A run that failed has the toast, then the project row’s failure glyph, then the diagnostic popover — three surfaces that already exist for exactly this, none of them the codebook’s. The card’s job is to say which codebooks are installed and help the researcher decide which are interesting, and it already carries enough to do that: tag count, reach, tentative count, and the two-tone bars on the details page.

3  The engine

One click, four terminal states. The LLM condition decides which. Red outlines are dead ends — states with no path back out.

Not installed button: Install click Running chip + progress ready Coded completed, full some batches died Partial completed, short no credit / all died Uncoded failed no key / local / ambiguous Uncoded no job — refused Review → Reviewed / applied the intended end no way out 409 already_applied retryable… in theory card says Uninstall retryable card says Uninstall All three lower-right boxes render IDENTICALLY in the navigator and on the page.
works, has a way forward dead end recoverable in principle, unreachable in the UI

4  What the card says

Settled: the card does not report run outcomes. Its job is to say which codebooks are installed and help the researcher decide which are interesting, and it already carries enough for that. An incomplete or failed run is an event belonging to the project — sidebar status line, failure glyph, diagnostic popover — all of which exist.

StateCardRun outcome lives
Not installed 28 tags · not installed—
Running 28 tags · ✦ Autocoding… — that the click landed, no more. Progress, elapsed and Cancel stay on the chip. project row + chip
Coded 28 tags · applied to 30 quotes in Ikea · 14 tentative tentative also renders as the pale segment of the two-tone MicroBar on the details page, with "N tentative + M accepted" on hover —
Partial unchanged from Coded — the shortfall is not the codebook’s business project row + popover
Failed / refused unchanged — 28 tags toast, then project row + popover

The line, rendered

Laws of UX
28 tags · applied to 33 quotes in Ikea
Laws of UX
28 tags · ✦ Autocoding…
Laws of UX
28 tags · applied to 30 quotes in Ikea · 14 tentative

“tentative”, not “undecided” — the word the threshold histogram already uses for its middle band (autocode.review.zoneTentative), and the name of the column behind it. The card was the only place in the product calling it something else, and the literal was hardcoded English in all 21 locales.fixed

5  Mockup coverage

Which existing mockups are trued to the code, and what they cover. The pattern is stark: every mockup covers the happy path or the global condition; none covers a codebook whose coding did not happen.

MockupLast edit Trued?Covers
out-of-credit-ux.html14 Jul yes Axis A only. §2 the pill + popover (matches OutOfCreditPill.swift exactly). §3 already settled that reaching for AutoCode while out of credit gets “nothing new — the pill is enough”, which is the decision this whole thread re-derived.authority
codebook-v2-autocode-button.html31 Aug yes Install → Uninstall → Review arc. Happy path only — no failure state appears anywhere in it.
codebook-library-states.html1 Aug yes enabled / disabled only.
provider-status-glyph-vocabulary.html7 Jun superseded Explored three treatments for the provider dot; a fourth shipped — colour plus an always-visible localised label. Six ProviderStatus cases ship, this names five. The a11y reasoning stands.banded
mockup-autocode-lifecycle.html19 Feb stale Not a state map at all — my earlier reading of it here was wrong. It is a nine-step storyboard of the v1 flow (“Codebook tab — before import”, then “after import, AutoCode available”), where importing a codebook and coding with it were separate acts. 0.29.0 made Install be apply. Banded SUPERSEDED.banded
codebook-v2-messages.html30 Aug implemented Its message taxonomy is the live one, and its “what this needs that does not exist” section drove ae050e56. Read that as a diagnosis of a fixed problem; the CodebookPanel reference describes the v1 lens it was written to replace.banded

6  Decided

  1. A codebook is installed or not installed. A run is a per-project activity, not a codebook property — the rule the codebase already applies to copy, where the toolbar pill was removed in favour of the row.
  2. The card does not report run outcomes. Not partial, not failed, not a coverage count. Its job is to say which codebooks are installed and help the researcher decide which are interesting; it already carries enough for that. An incomplete run is the project’s status, and the sidebar row, its failure glyph and the diagnostic popover already exist for it — the popover specifically to help diagnose a failed run.
  3. Running shows on the card as ✦ Autocoding… and no more — direct manipulation, feedback on the thing that was clicked. Progress, elapsed and Cancel stay on the chip. Existing vocabulary (autocode.chip.coding).
  4. The card says “tentative”, matching the histogram band and the tentative_count column. Previously “undecided”, and hardcoded English in all 21 locales.shipped
  5. Uninstall during a run cancels, then removes — not a disabled button with a tooltip. Cancel already exists on the chip, and design-pipeline-diagnostic-popover.md records that .help(...) tooltips were dropped from this surface family.

7  What is left to build

Smaller than it looked, because §6.2 removes the coverage rendering that was driving most of it. The card needs a boolean — is a job running for this framework — not the job’s numbers.

  1. The lens needs to know a job is running. getAutoCodeStatus has zero call sites in the island, its navigator or its page, so ✦ Autocoding… has nothing to render from.
  2. The sidebar has no case coding. 23 ProjectSubtitle cases — running, queued, copying, importingBatch, completedPartial — and not one about coding. “The project row shows the run” is the right model and not yet the behaviour, and it is where §6.2 sends everything.
  3. AutoCode has no cause to put in the popover. The popover eats PipelineState from the pipeline events log — stages, per-session causes, a terminus — and an AutoCode job has none of those. Routing its failures there means giving it a cause in that vocabulary, not giving the popover a second data path.
  4. A failed or partial job still cannot be re-run. 409 already_applied on a completed job, and reapply_to_new_quotes only codes sessions imported after the watermark. Measured cost of simply re-running: £0.042 per batch (median 7,701 in / 1,999 out over 51 real batches), so £0.17 for a 78-quote project — cheap enough that relaxing the guard beats building resume machinery. Note partial has never occurred in 25 logged runs; all 5 failures were total.