# REPORT — escrow recovery-code wordlist fix (TASK-E Part 1), 2026-07-21 **Version: v0.93.0. Baseline `935904f` (v0.92.1), clean tree. NOT built or deployed** — no operator action needed today; this rides the next agent publish train. ## What was wrong The EFF large wordlist contains exactly four entries that themselves contain the hyphen the words are joined with: `drop-down` (L2009), `felt-tip` (L2528), `t-shirt` (L6640), `yo-yo` (L7748). Drawing one produced a recovery code that reads as **11 words instead of 10** — ambiguous to transcribe in the one situation R exists for, a customer reading a code back during a disaster. **The "known flake" was this defect, not a flaky test.** Per code the hit rate is `1 − (7772/7776)^10 ≈ 0.51%`; over the test's 50 draws that is ≈ **23%**, which is exactly the "fails ~1/5" that had been documented in the `felhom-testing` skill and re-run past for weeks. `felhom.eu/REPORT.md` §6 had it at 3/8 in one session. ## The fix `joinSafe` filters those four out of the effective wordlist at init. Generation-only. | | before | after | |---|---|---| | draw space | 7776 | **7772** | | 10-word code entropy | 129.248 bits | **129.241 bits** | | margin over the 128-bit floor | 1.248 | **1.241** | The cost is **0.007 bits**. Both numbers are asserted in `TestEntropyFloorSurvivesFiltering`, so a future wordlist swap cannot quietly move the floor. **Every recovery code already issued remains valid**, and this was verified rather than assumed: R is consumed as a whole passphrase by the PBS scrypt KDF (`Wrap`/`Unwrap`), and `grep` confirms nothing in the consume path ever splits it. The joiner, the word count and the KDF path are untouched. ## Tests `TestGenerateRecoveryCode_EntropyAndFormat` now counts words by **generation count**, not by splitting the joined string — conflating those two is what made it flake. It asserts the segmentation property separately, because that is the property `joinSafe` actually buys. New in `wordlist_test.go`: the four filtered words are pinned by name (a wordlist swap that changes the set fails loudly); the entropy numbers above; and a production-wiring test that drives the exported `GenerateRecoveryCode` 500×, so the fix is proven at the entry point and not only in the helper. ### Red-proof — RUN, both deterministic and probabilistic halves Pre-fix shape restored (`var wordlist = parseWordlist(wordlistRaw)`), suite re-run: ``` --- FAIL: TestWordlistLoaded effective wordlist should be 7772 words (7776 EFF - 4 hyphenated), got 7776 --- FAIL: TestGenerateRecoveryCode_EntropyAndFormat joined code must segment into 10 words, got 11 (a drawn word contained "-") --- FAIL: TestEntropyFloorSurvivesFiltering filtered entropy moved: 129.248, expected 129.241 --- FAIL: TestGenerateRecoveryCode_NeverContainsAmbiguousWord code 193 segmented into 11 parts, want 10 ``` Fix restored → green; `git diff` clean. `TestGeneratedCodeSegments_FilteredVsUnfiltered` is the **deterministic** companion: it drives the generator against a fixture list where every word is hyphenated, so the pre-fix defect reproduces with probability 1 rather than ~1/5, and shows the same list through `joinSafe` refuses to generate at all. ## Gates `go build ./... && go vet ./... && go test ./...` — **green**. The ex-flaky package was additionally run 10× consecutively: 10/10 pass (it previously failed ~1 in 5). ## Observations - The `felhom-testing` skill's "Known flake … re-run before diagnosing; it is not a regression" line is now removed. It had been actively harmful: it told every future session to dismiss a true positive. Replaced with the generalised lesson — "known flake, just re-run it" is a diagnosis and needs evidence like any other. - `WordlistSize()` now reports the **effective** (filtered) draw space, 7772. `WordlistFilteredOut()` is new, for audit. Any external consumer expecting a literal 7776 would need updating; there is none outside the tests.