uttrflow / uttrflow/uttrflow-swift
The guard's ASCII-only tokeniser leaves Devanagari to the base checks, and negatingWords holds no Hindi, so a dropped Hindi negation is caught by nothing
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
**P2 — meaning change.**
### What happens
A Hindi dictation is checked only for emptiness, a preamble, a growth ratio and invented digits. A model that translates a clause, drops one, or turns "नहीं" into its opposite while romanising is accepted and inserted.
### Why
The tokeniser is ASCII-shaped, so a script it cannot split is skipped rather than handled, and `GuardVerdict` has no value meaning "I could not judge this" — only `accepted` and `rejected`.
### What the code shows
The code says what the finding says. `isPlain` is `matching.allSatisfy(\.isASCII)` (:121), the pool filters on it (:131) and the kept-token loop requires it (:137-138), so a Devanagari draft contributes no tokens to either side and `grammarVerdict` reaches `.accepted` having compared nothing; `functionWordChurn` (:218) filters on `isPlain` too, so the churn is zero as well. `negatingWords` (:208-212) holds no Hindi word in either script. The prompt does ask for romanisation (PromptContract.swift:18-20), which is the most invasive thing the model does to the text. What the finding does not say is that this is deliberate and tested: the comment at :120 states it, and `skipsDevanagari` (Tests/UttrflowAITests/MeaningPreservationGuardTests.swift:362-365) asserts the acceptance. So it is a known limitation rather than an oversight, which is why the finding's proposed remedy — make the guard fail closed — is the wrong shape: fail-closed on an unreadable script would reject every Hindi rewrite until a transliteration table existed, on a path where a rejection costs the whole model answer. One part of the finding is a genuinely cheap fix, and worth separating out: `negators(in:)` (:203-205) does NOT filter on `isPlain`, so adding the Devanagari and romanised Hindi negations to `negatingWords` closes the negation half immediately, in both directions, with no tokeniser change at all.
### Where
- `Sources/UttrflowAI/MeaningPreservationGuard.swift:121`
- `Sources/UttrflowAI/MeaningPreservationGuard.swift:131`
- `Sources/UttrflowAI/MeaningPreservationGuard.swift:208`
- `Sources/UttrflowAI/PromptContract.swift:18`
- `Tests/UttrflowAITests/MeaningPreservationGuardTests.swift:362`
### Fix direction
Split this into the part that is safe now and the part that needs measuring. Now: add the Hindi negations in both scripts to `MeaningPreservationGuard.negatingWords` — नहीं, ना, मत, nahi, nahin, na, mat. `negators(in:)` reads `matching` without an `isPlain` filter, so this works for a Devanagari draft against a romanised rewrite with no other change, and it needs only corpus cases for a dropped and a kept Hindi negation. Later, and only with the Hindi corpus in front of you: replace the `isPlain` predicate scattered through three call sites with Unicode word segmentation in the tokeniser, so Devanagari tokens exist and can be aligned, and judge a transliteration as a `.formChanged` edit under a `Transliteration` relation living in one table beside `IrregularVerbForms`. Do NOT make an unmappable token an automatic rejection and do not make `.undecided` mean rejection by default — decide that from the bake-off, per language, because on-device transcription and the Hindi path are the product's differentiator and a guard that refuses what it cannot read would silently disable the model for every Hindi user.
### Principles
Single responsibility: script handling belongs in the tokeniser, not as an `isPlain` filter at three call sites. Open/closed: a new script becomes a new relation table rather than a new branch. Fail-closed is right as a default only where a corpus says the cost is acceptable.
---
Found by a code-path audit of the dictation pipeline. Traced in the source and checked against `Docs/` by a second reader; **not reproduced at runtime**, which is why this carries no `confirmed` label. Any rule or prompt change proposed above is measured against the corpus with `make bakeoff` before it lands.
Contributor guide
Research direction
Start with negatingWords and negators(in:) in Sources/UttrflowAI/MeaningPreservationGuard.swift, then read the existing cases in Tests/UttrflowAITests/MeaningPreservationGuardTests.swift. Add corpus cases for dropped and preserved Hindi negations in Devanagari and romanised forms, and run the relevant test suite. Done means these negations are detected without changing the ASCII tokeniser.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- ai, testing-qa
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100