Repository navigation
Releases: ashlrai/lexicon
Release list
v0.5.4
Full Changelog: v0.5.3...v0.5.4
v0.5.3
Thirty-four commits since 0.5.2. If you are running a desktop app ("Fix everywhere") or you keep anything in your lexicon you would not like to retype, this is the release to take.
npm still serves 0.5.1. The publish step is skipped in CI because the account is behind a hardware key, so npm i -g @ashlr/lexicon gets 0.5.1 until that happens by hand. Until then, install from this tag or from Homebrew:
npm i -g github:ashlrai/lexicon#v0.5.3
brew install ashlrai/tap/lexicon
Why you would reinstall
Both desktop apps read the text of every field you focused before deciding whether they were allowed to. The watcher pulled the focused element's full value and cached it, refreshed four times a second on Windows and twice a second on macOS, while the exclusion list and the secret-field heuristic ran a layer above where all they could do was decline to use text the process already held. A password manager's field, a vault note or a TOTP box in an app you had not excluded sat in memory for as long as it held focus. Nothing was sent anywhere and nothing was logged, so the exposure was a crash dump or an attached debugger, which is exactly the reach the threat model says we do not give it, and SECURITY.md claimed the opposite. The decision now runs in front of the read on both platforms, on every read path, and a test drives a field that counts how often anything asked for its value and asserts the count stays at zero through every refusal case.
Turning "Fix everywhere" off did not turn it off. The watcher kept reading and caching every focused field and merely declined to correct it. That switch is the mitigation the docs offer as the last resort, so off now means the watcher stops resolving focus and reads nothing at all. On macOS, excluding an app never reached the gate either: the settings sinks read the exclusion list back off the object they were handed, so the list pushed into the engine was the one from before your change, and the app you had just excluded went on being read and rewritten.
The undo hotkey was the one path that read a field without asking. On both platforms it went to the field's value first and consulted the gate afterwards, so excluding an app mid-focus stopped neither the read nor the write.
Both write paths could leave half a correction in your field, with no undo, and tell you the write had failed. The keystroke write was a loop on Windows and an unaccounted chunk sequence on macOS, so a failure partway through left the replacement half in and your own span already gone, while the engine reported "field changed" and recorded nothing. There is one write call now, capped, with backspaces and retype together so a deletion cannot succeed alone; anything longer goes through a whole-value write that cannot half-finish; and what actually landed is read back off the field across the same settle window rather than inferred from a count that only says what was queued.
voice --toggle deleted finished recordings. Every toggle capture is spawned with -t 600, so one nobody stops ends itself at ten minutes. The next press found a pid that was gone, called the state stale, deleted the WAV without opening it, printed "recording" and exited 0. A complete, finalized ten-minute capture, removed unread. Four further ways a recording was thrown away on a guess are fixed alongside it, a recycled pid can no longer send the stop signal to a stranger's process, and on Windows every toggle recording under about eight seconds was being lost outright to a 256 KB ffmpeg buffer. Nothing deletes a capture that holds bytes any more.
Concurrent writes to the lexicon silently lost terms. Twelve concurrent lexicon add calls each printed created TermN and left one term behind. Every read-modify-write now runs inside a lock that works between processes, is re-entrant, breaks at once when its owner has died, and throws rather than overwriting. Three callers above the lock were doing their own read-decide-write and are fixed too.
One import of a non-UTF-8 file left the lexicon unparseable with no backup, and every message you then saw was false. A UTF-16LE file, which is what PowerShell's Out-File and Notepad's "Unicode" write, died against the schema naming the destination lexicon, so the error blamed a file you had never touched. A Latin-1 file was worse: exit 0, no warning, and Caf<U+FFFD> stored permanently as the canonical spelling. Encoding is decided from the bytes now, the serialized result is parsed and validated before anything is replaced, and the previous contents are kept at <path>.bak. With a broken file, doctor used to say there were no terms and to run lexicon setup, both false; it now quotes the parse error and points at lexicon edit. normalize exits 1 instead of 0. The hook, which must stay silent and must exit 0, now tells the agent the file does not parse instead of going quiet.
The setup wizard's repo harvest was teaching your lexicon to rewrite ordinary English. It split PascalCase symbols into aliases, so TimeInterval arrived carrying time interval and "let us set a time interval of five minutes" came back with a class name in it. Measured over forty-five dictated sentences across four repos, the list that step built damaged 5.0% of them. It invents no word boundaries now and proposes a name only when the repo says it somewhere other than its own source. Same corpus after: 0.6%. A three-file project that used to yield nothing but its own directory name and the git author now finds four of the five services it actually runs on. Step 3 also defaults to no, because it is a write inside your repository plus a trust decision offered ninety seconds in.
The guard that keeps text quoting a misspelling from being flattened into one spelling was rebuilt twice and is now pinned by a generated suite rather than a hand-picked list: 36,540 quoting cases and 243 dictation cases, both directions, nothing skipped. It failed 27,873 of those quoting cases before. Neither benchmark moved, which is the point, because the corpus cannot see any of this. Two spans the matcher was widening past the word it matched are fixed ("cooper netties i.e. Terraform" came out as "Kubernetes. Terraform"), and indented Markdown code blocks are skipped now.
Still true, and worth saying plainly
The Windows tray app's UI Automation half has never run on real Windows. Everything under apps/windows/src/LexiconBar.App/FixEverywhere/ uses UI Automation and SendInput to read and rewrite text in whatever application has focus. It compiles, and 214 headless C# tests run in CI on Linux and on windows-latest, but no line that touches UI Automation has been executed on a real Windows desktop, and CI cannot close that gap: a GitHub runner has no interactive session, so there is no focused element, no foreground window and no caret. That is why no release ships the binary. Get it from the CI artifact and read docs/WINDOWS-APP.md before granting it anything.
The refusals are a name and label heuristic, not a guarantee. On both platforms a password manager running under an executable name we do not recognize, holding a field with no revealing label, is not distinguishable from a text editor. Exclude your manager explicitly rather than relying on the defaults, and turn "Fix everywhere" off if that trade is not one you want. Off now means off.
LexiconBar.app.zip is signed ad-hoc and not notarized. Gatekeeper blocks the first launch (right-click then Open, or xattr -d com.apple.quarantine), and the Accessibility grant is bound to a cdhash that changes with every release, so each update loses the grant until you remove the row in System Settings and add the app again.
Gate
927 TypeScript tests, 228 Swift, 214 headless C#, all green on ubuntu (Node 20, 22, 24), windows and macos. Typecheck, link check, fact check, bundle drift check, CLI doc regeneration, extension build and site build all clean. The plugin bundle was built in a clean checkout and verified to contain no absolute path to anyone's home directory.
The full record, every claim with the reasoning behind it, is in CHANGELOG.md.
Full Changelog: v0.5.2...v0.5.3