# v2.33.0 — the release found by running what we shipped

**Released 2026-08-27.** Upgrade:

```sh
sudo snap refresh yazses     # snap
pipx upgrade yazses          # or: uv tool upgrade yazses
scoop update yazses          # Windows
```

18 commits, almost all of them Windows correctness. The theme is narrower than last
release's and less comfortable: the test suite could not see any of it. Two of these
defects were found by installing the *published* 2.32.0 build on a clean Windows Server
host and running it, and one had been failing every CI leg for a week while passing in
every local checkout.

## The bundled `.exe` and `.app` entered a different CLI from the one we ship

`yazses doctor` from the Scoop-installed 2.32.0 binary died with a `UnicodeEncodeError`
on `'✗'` — hours after that exact crash had been fixed and released.

The console script installed by pip runs `yazses.cli:main`. The PyInstaller entry point
is a separate file, `src/yazses/__main__.py`, and it called `yazses.cli:app` — the Click
group itself, one layer below. `main()` is where the Windows console is put into a
printable encoding, where the help text is escaped, and where an unsupported platform is
reported as a message instead of a traceback. **None of those ran in any bundle we have
ever shipped**, on Windows or macOS, so three separate fixes were released and reached
nobody who installed the `.exe` or the `.app`.

The entry point now calls `main()`, and seven AST-level tests pin it there: the bundle
entry may not reference `cli.app`, `pyproject.toml` must still map the console script to
`cli.main`, and `main()` must still do all three of the things it does. Two routes into
one program is the kind of duplication that stays correct until the day it doesn't.

## A machine with no audio device could not import YazSes at all

`audio/recorder.py` imported `sounddevice` at module scope. `sounddevice` calls
`Pa_Initialize()` on import, and on a host with no sound card that does not return an
empty device list — it raises `PortAudioError`. So `import yazses.core.daemon` was
impossible on any such machine, which includes most CI runners, most containers, and
every server.

The import is now deferred to first use behind a small accessor. The measured effect on
a bare Windows Server 2022 host: the suite went from **5666 tests collected with 46
collection errors** to **13449 passed, 0 failed, 0 errors**. Forty-five of those 46
errors were this one import.

The remaining two were a related lesson. `pytest.importorskip` catches only
`ImportError`, so a dependency that fails at *load* — `PortAudioError`, `OSError`,
a missing DLL — is not skipped but raised, and a single collection error abandons the
entire run. Those tests now go through a shared helper that skips on any exception.

## `yazses report` left a Windows account name in clear

The diagnostic bundle redacts your account name with a pattern wrapped in `\b…\b`. `\b`
asserts a *transition* between a word and a non-word character, so next to a symbol it
demands a word character that isn't there. A Windows machine account name ends in `$`,
and could therefore never match: it went into the report in clear, alongside `mohsen.`
and `user-` and anything else ending in punctuation.

The boundaries are now applied only on the ends that are word characters. Short names
still do not match inside longer words. The test that should have caught this was
rebuilding the pattern from its own escaped form and passed on the broken code; it now
pins a name deterministically and asserts that the *old* pattern still fails, so the
demonstration cannot quietly stop demonstrating.

## CI was red on all eight legs for a hook that is entirely correct

`hooks/sitemap_dates.py` runs `git log -1 --format=%cs -- docs/index.md`. The test job
checked out with `filter: tree:0`. A **treeless** clone cannot answer a path-limited
`git log`: deciding which commits touched a path needs the commit trees, so git fetches
them one at a time from the promisor remote, the hook's timeout expires, and the date
comes back empty. Measured against real clones of this repository — `tree:0` fails with
*"could not fetch tree from promisor remote"*, `blob:none` answers in milliseconds.

`blob:none` keeps the trees and skips file contents, which is where nearly all of the
size is. A guard now fails the build if the checkout goes treeless again, and pins the
hook's query so the workflow comment cannot outlive its reason.

## Scoop installs the native build on Windows on ARM

The Scoop manifest declared only a 64-bit entry, so Windows-on-ARM users got the x64
build under emulation even though an ARM64 installer is published. The manifest now
carries an `arm64` architecture — and the generator **removes** it when a release has no
ARM64 asset, because that build leg is `continue-on-error` and a stale entry would 404
instead of falling back to x64.

## Also in this release

- **Two workflow-shell test files asked the wrong question about the host.** They guarded
  on `shutil.which("bash")`. On a GitHub Windows runner that resolves to the WSL
  launcher, which is not a shell; on macOS bash is real but the step calls GNU `timeout`,
  which is not there. They now guard on the condition the workflow itself states.
- **The last twelve Windows test failures were the host, not the product** — tests that
  read the machine they ran on and meant something different on each one.
- **`yazses doctor > log.txt` crashed on Windows** when output was redirected to a file
  under a legacy code page.
- **`doctor` called a Windows install healthy** while nothing could actually be
  transcribed, because it never checked that the decoder could load.
- **`doctor` told Windows and macOS users to run `apt`.**
- **The release checksum could describe a binary that no longer existed** — a wait loop
  that counted filenames was satisfied instantly on a re-run.

## Upgrade notes

Nothing in this release changes configuration, on-disk formats, or defaults. If you
installed the Windows `.exe` or the macOS `.app` from 2.32.0 or earlier, this is the
first build in which `yazses doctor` works at all — that is the upgrade worth taking.
