# Local-only strip: audit

Status: **done**. The strip landed on 2026-09-19. The plan below is kept
as the record of what was removed and why; see *Outcome* for what it
actually cost.

Dictate is no longer a commercial product. It is a local-first dictation app
worked on when there is time. Everything that exists to support accounts,
subscriptions, hosted transcription, cloud sync and cloud deployment is dead
weight and comes out.

The goal after the strip: **a nice working package for local dictation**, so a
user installs one thing and it works, instead of assembling a UI and a model
runtime themselves. No account. No API key. No network dependency in the
transcription path.

## Outcome

The strip removed **27,064 lines across 107 files**, deleting 53 files
outright — more than the 20,020 lines across 50 files estimated below.

| Area | Files | Lines removed |
| --- | ---: | ---: |
| `src/` | 36 | 12,594 |
| `tests/` | 37 | 11,431 |
| `ui/` | 10 | 2,201 |
| `scripts/` | 3 | 606 |
| docs, CI, packaging | 21 | 232 |

The estimate was low for three reasons the audit did not anticipate:

1. **The front-end was not counted.** `ui/` contributed 2,201 lines,
   dominated by `AccountDialog` and its tests.
2. **Deleting a module means fixing every test that touched it.** The
   tests row is nearly as large as `src/`, and most of that was editing
   surviving tests rather than deleting cloud ones.
3. **CI referenced the deleted code.** Three workflows ran the cloud-sync
   audit and volume smoke, invoked deleted test modules by name, and
   carried a whole `linux-user-sync-smoke` job that `package`, `publish`
   and `release` gated on.

Two things had to be kept that the audit listed for deletion. The update
UI — version rows, the Stable/Beta channel selector, the update actions —
lived inside `AccountDialog`, so that dialog was replaced by a smaller
`AboutDialog` rather than deleted. And `scripts/msstore-submit.py` stayed
once Microsoft Store distribution was confirmed as staying.

One audit claim was wrong: `whisper-cpp` was listed as a surviving local
backend, but `whisper_cpp_backend.py` has no `BACKEND_REGISTRY` entry, no
`DEFAULT_MODELS` entry and is absent from the `SttBackend` literal. It is
not selectable, and making it so would be new work, not preservation.

After the strip: **589 Python tests and 85 UI tests pass.**

## Headline (as estimated, before the work)

Removing the cloud surface deletes roughly **20,000 lines across 50 files**,
about **36%** of `src` + `tests` combined (55,257 lines today). A further
~10,500 lines across 12 files need editing rather than deleting.

There are no existing users on the cloud side, so nothing needs migrating,
deprecating or keeping alive for compatibility. The code can just come out.

This is not only "delete the cloud code". A large part of the win is that
machinery which exists *because* the cloud can fail also stops being needed.

## Tier 1 — delete wholesale

These have no local-only purpose. Nothing survives them.

| Area | Files | LOC |
| --- | ---: | ---: |
| `src/dictate/pro/` — accounts, Stripe, hosted server, relay, signing, email | 15 | 6,793 |
| `src/dictate/api_keys.py`, `api_keys_dialog.py` | 2 | 1,346 |
| `src/dictate/sync.py`, `sync_engine.py` | 2 | 942 |
| `src/dictate/stt/{openai,gemini,xai}_backend.py` | 3 | 818 |
| `src/dictate/provider_supervisor.py` | 1 | 387 |
| Tests covering all of the above | 24 | 9,128 |
| `scripts/` — cloud sync audits and smokes | 3 | 606 |
| **Total** | **50** | **20,020** |

`scripts/msstore-submit.py` (307 lines) is **not** in this list. Microsoft
Store submission stays — see Tier 3.

### Why `provider_supervisor.py` goes

Its own docstring says it "handles graceful degradation when the remote fails,
and schedules automatic recovery probes using class-aware exponential backoff",
with on-device as "the always-available floor". With no remote, there is no
degradation to manage and no probe to schedule — the floor becomes the whole
building. 387 lines of supervisor plus 1,188 lines of tests exist purely to
survive a network that local-only never touches.

This is the shape of the whole change: the cloud did not just add features, it
added failure modes, and the code handling those failure modes is larger than
the features were.

### `src/dictate/pro/` detail

`store.py` alone is 80 KB. With `client.py` (51 KB), `service.py` (36 KB) and
`server.py` (31 KB), four files carry most of the weight. `stripe_handler.py`,
`plans.py`, `browser_auth.py`, `email_delivery.py` and `signing.py` are
subscription and identity plumbing with no local equivalent.

Note `dictate-pro-server` is a console entry point in `pyproject.toml` — a
hosted server shipped inside a desktop app. That goes with the package.

## Tier 2 — edit, do not delete

These are load-bearing for local operation but currently reference cloud paths.

| File | LOC | What to remove |
| --- | ---: | --- |
| `src/dictate/ui_server.py` | 2,182 | `ProClient`, `ProviderSupervisor`, `PRODUCT_DESTINATIONS`, sync endpoints, all API-key endpoints |
| `src/dictate/daemon.py` | 1,938 | `ProviderSupervisor` wiring |
| `src/dictate/__main__.py` | 1,573 | `ProClientError`, supervisor, sync settings, API-key CLI commands |
| `src/dictate/tray.py` | 1,351 | API-key menu items and dialog launch |
| `src/dictate/stt/factory.py` | 742 | Cloud backend registration; `resolve_default_local_backend` already exists and becomes the only path |
| `src/dictate/windows_control.py` | 687 | API-key panel |
| `src/dictate/note_store.py` | 509 | `SyncOutbox` |
| `src/dictate/ui_launcher.py` | 400 | `ProviderSupervisor` |
| `src/dictate/config.py` | 390 | Sync record-category settings; `xai` hotword branch |
| `src/dictate/history.py` | 230 | `SyncOutbox` |
| `src/dictate/stt/__init__.py` | 87 | Cloud backend exports |

`ui/src/App.jsx` is the front-end equivalent — 185 lines match account / sync /
billing / API-key identifiers, and `ui/src/ipc.js` has 51. `productDestinations.js`
goes entirely.

## Tier 3 — docs to retire

Move to `docs/archive/` rather than delete, so the history stays readable:

- `DICTATE_PRO_CLOUD_SYNC_PLAN.md`
- `dictate-pro-subscription-architecture.md`
- `desktop-browser-signin-architecture.md`
- `dictate-pro-terms.md`
- `msstore-in-app-subscriptions.md` — the subscription mechanism only; the
  Store channel itself stays
- `deck-sections-and-dictate-space-spec.md`
- `platform/dictate-platform-inventory-v1.md`
- `platform/arc-forge-backend-handoff-v1.md`
- `contracts/dictate-platform-v1.json` (2,048 lines)

`dictate-privacy-policy.md` needs a rewrite, not an archive: with no account and
no network transcription path, the policy becomes short and much stronger.

## What stays

The local transcription path is already first-class, which is why this strip is
feasible rather than a rebuild:

- `stt/parakeet_backend.py`, `parakeet_pyannote_backend.py`,
  `parakeet_speaker_backend.py` — local Parakeet and diarisation
- `stt/faster_whisper_backend.py`, `whisperx_backend.py`
  (`whisper_cpp_backend.py` exists but is not registered — see Outcome)
- Audio capture, hotkey, tray, history, lexicon, note chunking, outputs
- Installers, updater, packaging
- `resolve_default_local_backend` / `resolve_default_local_model` in
  `stt/factory.py` — the local default already exists

Runtime dependencies in `pyproject.toml` need no change: they are already
local-ML (`faster-whisper`, `onnx-asr`, `onnxruntime`, `numpy`). There is no
Stripe or web-framework dependency to drop, because the hosted server was built
on the standard library. `cryptography` was there only for sync record
encryption and has been dropped: after the strip nothing in the repository
imports it.

## Decisions

**Microsoft Store stays.** It remains the intended public Windows channel. The
listing is overdue for an update and gets one once the first stable local-only
build is done. `msstore-publish-msix.yml`, `msstore-api-smoke.yml`,
`windows-msix-store-bundle.yml`, `scripts/msstore-submit.py`,
`msstore-automation.md` and `msstore-listing.md` all stay; the listing copy
needs rewriting for local-only positioning when that update happens. Only the
in-app subscription content goes, since there is nothing to subscribe to.

**No migration needed.** There are no existing users on the cloud side, so
there is no saved `stt_backend: openai|gemini|xai` config in the wild to fall
back gracefully from, and no stored API keys in an OS secret store to clear.
Both of these would otherwise have been required work; neither is. The cloud
code can be deleted outright rather than deprecated.

## Open question

**`history`/`note_store` outbox.** Both take `SyncOutbox`. Confirm removing it
leaves local history intact — it should, but the write path needs reading
before cutting. This is answerable from the code and does not block starting.

## Suggested sequence

Each step should land green on its own.

1. Delete `pro/` and its tests; drop the `dictate-pro-server` entry point.
2. Delete cloud STT backends; collapse `factory.py` onto the local default.
3. Delete `provider_supervisor.py`; unwire from daemon, ui_server, ui_launcher,
   `__main__`.
4. Delete sync; unwire `SyncOutbox` from `history.py` and `note_store.py`.
5. Delete API-key management; strip the tray, control panel and UI surfaces.
6. Archive docs; rewrite the privacy policy; drop the in-app subscription doc.
