Conversation
Captures the silent training signal of in-place edits to pasted transcripts (SPEC-022 PR-A). After a successful paste-at-cursor, attach a one-shot AXObserver to the focused field; on focus-change or 60s timeout, diff the final value against the pasted transcript token-by-token and append surviving substitutions to ~/Library/Application Support/OpenQuack/correction_candidates.json (deduped, capped at 500, oldest-lastSeen-evicted). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
larryxiao
marked this pull request as ready for review
May 13, 2026 16:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SPEC
SPEC-022 PR-A (Custom dictionary auto-learn from user corrections) — §2.2 post-paste observer and §2.3/2.4 candidate persistence only. PR-B (nudge) and PR-C (export) are out of scope.
Milestone
M2
Why
Most users immediately fix small Whisper errors in-place ("cloud code" → "Claude code") and OpenQuack throws that signal away today. PR-A captures it silently: after a successful paste-at-cursor we watch the focused field for the user's edit, diff it against the transcript, and persist the (wrong → right) pair. Once a pair has been seen
count >= 3times, PR-B will offer a one-tap "Add to dictionary" nudge — but nothing user-facing changes in this PR.Change
Sources/OpenQuackKit/Dictionary/(new folder):CorrectionCandidate.swift— the SPEC §2.3 struct.wrong,right,count,lastSeen,suppressedUntil?.dedupeKeylowercases both sides so casing variants collapse.CorrectionCandidateStore.swift—actor-backed JSON store at~/Library/Application Support/OpenQuack/correction_candidates.json. InjectablefileURLfor tests.record(_:)dedupes/merges counts, advanceslastSeen, caps at 500 entries with eviction bylastSeenascending.CorrectionDiff.swift— pureextractCorrections(rawTranscript:committedText:). Word-by-word positional alignment (SPEC explicitly OKs simple alignment over Myers). Drops: identical tokens, case-only diffs (avoids "i" → "I" flooding the store), pairs with case-insensitive Levenshtein > 3, and the small built-in stop-word set from SPEC §2.3.PostPasteCorrectionObserver.swift— one paste-event, one observer. Snapshots focused element + registerskAXValueChangedNotificationon it andkAXFocusedUIElementChangedNotificationsystem-wide. ADispatchSourceTimerenforces the 60 s deadline. All three paths (focus-change, timer, race) funnel through a singlefire(finalFieldValue:)guarded by adidFirelatch — observers/timer/source removed incleanupAfterFire(). The "edited segment" is the LCP-trimmed tail of both the transcript and the field so token alignment stays honest when the user edited past the prefix. Fields withoutkAXValueAttributesilently no-op (SPEC out-of-scope item).Sources/OpenQuackApp/OpenQuackApp.swift— single hook at the live-dictation paste seam (line ~651). After apasted == truefromPasteService.paste, we instantiate onePostPasteCorrectionObserveron the main actor and callstart(transcript:). No changes toPasteServiceitself; no callback signatures added.Tests in
Tests/OpenQuackKitTests/:CorrectionCandidateStoreTests— load/save, persistence across instances, case-insensitive dedupe + count merge, distinctrightvalues stay separate, 500-cap eviction bylastSeenascending, observer test-seam happy path.CorrectionDiffTests— single + multiple substitutions in order, identical / empty strings, case-only filter, case-folded edit distance, edit-distance boundary and over-threshold, stop-word filter onwrongside, timestamp propagation, segment-extraction helper.Tests
CorrectionCandidateStoreTests(8),CorrectionDiffTests(15)DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build && swift testgreen locally — 76 tests, 0 failures~/Library/Application Support/OpenQuack/correction_candidates.jsonPrivacy impact
Privacy contract is preserved.
(wrong, right, count, lastSeen, suppressedUntil?)quadruples persist — no audio, no full transcripts, no timestamps beyondlastSeen.kAXValueAttribute(password fields, some Electron apps) silently no-op.Out of scope
suppressedUntilis persisted by this PR for PR-B's "Not now" path.OpenQuackApp.swift:844): the recovery path auto-pastes old recordings on launch when the user isn't actively in the field. Not the same user-intent signal as a live dictation edit.AXObserverevent can't be driven in CI, so the observer class exposestestOnlyFire(transcript:finalFieldValue:)for the diff → store wiring and the AX-registration itself is manual-test-only.🤖 Generated with Claude Code