Skip to content

fix(voice): leave a term the vocabulary spells two ways by case as heard - #607

Merged
mrgoonie merged 2 commits into
mainfrom
fix/590-case-ambiguous-terms
Oct 7, 2026
Merged

mrgoonie merged 2 commits into
mainfrom
fix/590-case-ambiguous-terms

Conversation

@mrgoonie

@mrgoonie mrgoonie commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #590

Problem

The session vocabulary kept one spelling per lowercase key. With both UserService (a class) and userService (an instance) in a session, the transcript normaliser re-cased a heard term into whichever spelling won the key, turning one real symbol into the other.

Change

  • buildRecognitionContext (packages/voice-adapters/src/coding-vocabulary.ts) keys terms by their exact spelling, so every spelling the session itself uses is kept. This is a behaviour change only when the session spells a word two or more ways by case.
  • Glossary against session, per lowercase key:
    • the session spells the word one way only (a repository clarkcant, a provider gemini, a tool eslint): unchanged from main. The glossary entry and the session term contest by weight; the heavier spelling stays, a tie keeps the session's. So ClarkCant, Gemini and ESLint survive when the session term is not first in its list, and a dependency react still keeps react.
    • the session spells the word two or more ways by case (Voice normalizer: do not re-case a term whose spelling is ambiguous by case #590): new. Both session spellings stay and the glossary adds no third spelling beside them.
  • The normaliser (transcript-normalizer.ts) groups terms that differ only by case:
    • heard in that shared spelling (userservice, UserService, userService), the span keeps its heard case and is reported as a known technical term: no change, no abstention, so no pointless retry;
    • heard as spoken words (user service), it abstains with every spelling as a candidate instead of picking a case;
    • such a span heard in a session spelling still counts as technical evidence, as a single term did.
  • Unambiguous terms keep the feat(voice): restore the canonical casing of an exactly matched vocabulary term #574 behaviour (RedactSecrets -> redactSecrets in the same vocabulary).
  • ADR-003 casing rule updated in EN and VI as a pair.

Behaviour changes compared with main

Only where the vocabulary holds two or more spellings of a word that differ by case:

  • such a term heard in its shared spelling is left as heard (it was re-cased into one of them);
  • its spoken words abstain with all spellings as candidates (they were written in one of them);
  • a near match to such a term also abstains. Example: models Playwright plus tool playwright, "sửa playwrigt test" gives Playwright on main and abstains with both spellings here. A one-letter slip is no stronger evidence of case than the exact words, so abstaining is consistent and safe.

Everything else, including the glossary-against-single-session-spelling contest and the vocabulary sent to the recognizer in that case, is unchanged.

Evidence

Regression tests in packages/voice-adapters/test/transcript-normalizer.spec.ts:

  • the vocabulary keeps UserService and userService, each exact spelling once, and adds no glossary spelling beside a word the session spells two ways (PNPM + Pnpm);
  • the weight contest: a tool PNPM outweighs the glossary's pnpm (and its aliases go with it); a repository clarkcant listed after another project leaves the glossary's ClarkCant with its aliases;
  • "sửa clark cant trước" and "Clarkcant build lỗi" with repositories: ["other-app","clarkcant","web"] give ClarkCant;
  • "gemini model" with providers: ["google","gemini"] gives Gemini model;
  • "eslint lỗi" / "Eslint lỗi" with eslint after 30 other tools give ESLint lỗi;
  • userservice, UserService, userService and USERSERVICE are left as heard, with no change and no abstention, in Vietnamese and English sentences;
  • user service abstains with both candidates;
  • RedactSecrets is still re-cased beside the ambiguous pair.

The previous test "dedupes case-insensitively and keeps the glossary's aliases" only checked the CODING_GLOSSARY constant; it now asserts the built context in both directions of the weight contest. The new weight-contest tests fail against the previous head (71e9a44) and pass here.

Corpus bench (corepack pnpm --filter @clarkcant/voice-adapters bench:transcription, vi-en-coding-corpus.json, 84 utterances / 105 terms), output identical on main sources and this branch:

Stage WER CER Technical Term Error Rate Exact utterances Changes Abstained Regressions
raw 22.8% 3.8% 77.1% 26.2% - - -
normalized 3.7% 0.8% 16.2% 77.4% 64 1 0

Per kind (normalized) also unchanged: symbol 18/21, command 11/14, path 8/9, package 10/12, model 3/3, provider 6/7, acronym 9/9, glossary 19/24, version 2/2, branch 2/2, issue 0/2. Canonical references changed by the normaliser: 0. The corpus has no case-variant pairs and no multi-repository sources, so it exercises neither path; the unit tests above do.

The audio run (--audio ... --recognizer gemini-transcribe-live|gemini-live) was not run: external gate, no GEMINI_API_KEY in this environment.

Checks on head 4c00b7c: focused vitest packages/voice-adapters + apps/runtime/test/voice* (14 files, 231 passed), pnpm typecheck, pnpm invariants (14/14), eslint on the changed TypeScript files. pnpm verify was run on the previous head (71e9a44: 555 files passed, 1 skipped; 7363 tests passed, 37 skipped), not re-run locally on this head; CI covers it.

Notes

The session vocabulary kept one spelling per lowercase key, so with both
UserService and userService in a session the normaliser re-cased a heard
term into whichever spelling won the key, turning one real symbol into
the other.

The vocabulary now keeps every spelling the session itself uses, and the
glossary stays the floor: a session spelling still replaces the generic
glossary one, as before. The normaliser treats terms that differ only by
case as one group: heard in that shared spelling the term keeps its heard
case and counts as a known term, and its spoken words abstain instead of
picking a case. Unambiguous terms are re-cased as before.

Fixes #590
…sion spelling

The glossary now yields without a weight contest only when the session
spells a word two ways by case. A session spelling such as a repository
`clarkcant` listed after another project, a provider `gemini` that is not
first, or a tool `eslint` deep in the list again leaves the glossary's
ClarkCant, Gemini and ESLint, as before.
@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at 4c00b7c8761e3c48b751694bbe0481488f312f21, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie
mrgoonie enabled auto-merge (squash) October 7, 2026 19:32
@mrgoonie
mrgoonie merged commit 3c16e88 into main Oct 7, 2026
40 of 43 checks passed
@mrgoonie
mrgoonie deleted the fix/590-case-ambiguous-terms branch October 7, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Voice normalizer: do not re-case a term whose spelling is ambiguous by case

1 participant