Notes

I installed my own tool as a stranger would. Four things were wrong.

The install instructions had been tested. By me, on the machine where every dependency was already present, the key already in place and the program already running. That is not a test of the instructions. It is a test of my memory.

7 August 2026 · a clean install on the same computer · four defects, all fixed

Why this was worth doing. The tool had been published for hours, not months. Everything in it had been checked — except the one path every user takes first.

Finding 1

A setup window that no script can click

Without an API key, the first start opens a small window and waits for someone to paste one in. At a desk that is exactly right — better than telling people to copy a file and edit it by hand.

From a script, a remote session, or an AI agent doing the installation, it is useless in a specific and nasty way: the process starts, prints nothing, and never exits. No error, no timeout, no exit code. Measured: alive after twelve seconds, no output, no indication of what it was waiting for. Whoever automated the install would see a hung command and no reason.

The window is not the mistake. Having only the window is. Every step now has a path without one — write the key, check the installation, fail cleanly instead of waiting. Exit codes: 0 fine, 1 check failed, 2 bad invocation, 3 key missing and no window allowed.

Finding 2

A measurement that was true and applied to nobody

The changelog states the recording high-pass filter attenuates mains hum at 50 Hz by about 14 dB while leaving speech untouched. That number was measured and it is correct.

It also did not apply to a single person who followed the install instructions. The filter needs scipy, and scipy was missing from requirements.txt. The import sits inside a try block, so nothing crashed — the code fell back to removing the DC offset and logged one line at debug level, which nobody reads.

Measured on a fresh install, 16 kHz sine input, with and without the package:

InputWith scipyWithoutDocumented
30 Hz−31.0 dB0.0 dB−32 dB
50 Hz−14.2 dB0.0 dB−14.3 dB
110 Hz−0.2 dB0.0 dB−0.2 dB

The same applied to pycaw, which mutes playback during a recording. Both are genuinely optional in the sense that the program runs without them. Neither is optional in the sense that the program behaves as described.

A defensive try around an import converts a loud failure into a quiet difference in behaviour. That is usually what you want — but it means "it runs" and "it works as documented" stop being the same statement, and only the first one gets tested. Both packages are now listed, with a comment saying what is lost without them.

Finding 3

An exception on every window message, for months, unnoticed

The program keeps a hidden window to receive power events, so it can recover after standby. Its message handler ended by passing anything it did not handle to DefWindowProcW — called through ctypes without declared argument types.

LPARAM is pointer-wide. On 64-bit Windows, ctypes without argtypes tries to squeeze it into a 32-bit int and raises OverflowError: int too long to convert. Every window message. The reason nobody noticed: the exception is raised inside a ctypes callback, where Python cannot propagate it. It gets printed and discarded. Nothing crashes, nothing is reported — the message simply goes unanswered.

It surfaced only because the clean install was started with its output captured. On a normal start there is no console, so the text goes nowhere at all. Signatures are now declared and the return type is pointer-wide.

Finding 4

Two assumptions the instructions never stated

The first line of the install block was git clone. Windows on the test machine has no git — the very first command fails for anyone who has not installed it separately. It is an unremarkable prerequisite among developers and an invisible wall for everyone else.

The second line was pip install -r requirements.txt. On that machine, python resolves to 3.14 and pip to the scripts folder of a different, 32-bit 3.11 installation. Following the instructions literally installs every package where nothing will ever look for it, and the resulting error names a missing module rather than the real cause.

Instructions now name git and offer the ZIP download as an alternative, and use py -m pip throughout, which is guaranteed to match the interpreter that py starts.

What this cost, and what would have caught it

None of the four came from complicated code. Three came from the same blind spot: the author's machine already had everything, so the paths a newcomer walks were never walked.

The checks that existed did their job on what they covered. A legal-and-links linter runs on every push and caught a missing liability notice on the new page the same afternoon. It cannot catch a missing dependency, because a missing dependency is not visible in the source — only in the gap between the source and the machine it lands on.

The cheap fix is not a bigger test suite. It is one command that reports what is actually present:

py pushdictate.py --check

It names packages, key and write access, exits non-zero when the program would not start, and speaks JSON when something other than a human is asking. Three of the four findings above would have shown up in its output. The fourth — the ctypes overflow — needed the program to actually run with its output captured, which is the other half of the exercise and cannot be replaced by a check.