Skip to content

Commit 1d82631

Browse files
authored
feat(spacing): per-keystroke spacing-policy signals (#24 foundation) (#92)
Phase 2 of the spacing-policy epic (#14), per docs/SPACING_POLICY.md. Computes the two free signals from the existing suggestion results every keystroke (zero extra native calls), in a pure static helper for testability: - complete = typed word is a real dictionary word (mTypedWordValid AND source != user_typed) - prefixRichScore = fraction of candidates that are KIND_COMPLETION, [0..1] computeSpacingSignals(SuggestedWords) -> SpacingSignals; wired into setSuggestedWords to store mSpacingComplete / mSpacingPrefixRichScore. No behavior change yet — the signal-driven graceMs + two-gate Assisted tier (and the A11 insight readout) consume these next. SpacingSignalsTest: 6 cases (complete for real-dict / not for user-typed / not for invalid; prefix-rich fraction; empty). All green.
1 parent b9cd182 commit 1d82631

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ public final class InputLogic {
9595
private int mSpaceState;
9696
// Never null
9797
private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
98+
// #14 spacing-policy signals — recomputed every keystroke from the suggestion results at zero
99+
// extra native cost (see computeSpacingSignals / setSuggestedWords). Consumed by the upcoming
100+
// signal-driven grace + two-gate Assisted-tier logic.
101+
private boolean mSpacingComplete; // typed word is a real dictionary word
102+
private float mSpacingPrefixRichScore; // fraction of candidates that are completions [0..1]
98103
private final Suggest mSuggest;
99104
private final DictionaryFacilitator mDictionaryFacilitator;
100105
private SingleDictionaryFacilitator mEmojiDictionaryFacilitator;
@@ -1399,6 +1404,9 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) {
13991404
mWordComposer.setAutoCorrection(suggestedWordInfo);
14001405
}
14011406
mSuggestedWords = suggestedWords;
1407+
final SpacingSignals spacingSignals = computeSpacingSignals(suggestedWords);
1408+
mSpacingComplete = spacingSignals.complete;
1409+
mSpacingPrefixRichScore = spacingSignals.prefixRichScore;
14021410
final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;
14031411

14041412
// Put a blue underline to a word in TextView which will be auto-corrected.
@@ -1416,6 +1424,43 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) {
14161424
}
14171425
}
14181426

1427+
/**
1428+
* #14 spacing-policy signals derived from the current suggestion results, computed every
1429+
* keystroke at zero extra native cost.
1430+
* <ul>
1431+
* <li>{@code complete} — the typed word is a real dictionary word (valid AND not just
1432+
* user-typed). A confident "this is a finished word".</li>
1433+
* <li>{@code prefixRichScore} — fraction of candidates that are completions (longer words
1434+
* sharing this stem), in [0..1]. High = lots left to extend to (keep the word open);
1435+
* low = little left (safe to auto-commit).</li>
1436+
* </ul>
1437+
* Static + pure so it can be unit-tested without a live InputLogic.
1438+
*/
1439+
static final class SpacingSignals {
1440+
final boolean complete;
1441+
final float prefixRichScore;
1442+
SpacingSignals(final boolean complete, final float prefixRichScore) {
1443+
this.complete = complete;
1444+
this.prefixRichScore = prefixRichScore;
1445+
}
1446+
}
1447+
1448+
static SpacingSignals computeSpacingSignals(final SuggestedWords suggestedWords) {
1449+
final int n = suggestedWords.size();
1450+
if (n == 0) return new SpacingSignals(false, 0f);
1451+
final SuggestedWordInfo typed = suggestedWords.mTypedWordInfo;
1452+
final boolean complete = suggestedWords.mTypedWordValid
1453+
&& typed != null && typed.mSourceDict != null
1454+
&& !Dictionary.TYPE_USER_TYPED.equals(typed.mSourceDict.mDictType);
1455+
int completions = 0;
1456+
for (int i = 0; i < n; i++) {
1457+
if (suggestedWords.getInfo(i).getKind() == SuggestedWordInfo.KIND_COMPLETION) {
1458+
completions++;
1459+
}
1460+
}
1461+
return new SpacingSignals(complete, (float) completions / n);
1462+
}
1463+
14191464
/**
14201465
* Handle a consumed event.
14211466
* <p>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
package helium314.keyboard.latin.inputlogic
3+
4+
import helium314.keyboard.latin.SuggestedWords
5+
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
6+
import helium314.keyboard.latin.dictionary.Dictionary
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertFalse
9+
import org.junit.Assert.assertTrue
10+
import org.junit.Test
11+
12+
/**
13+
* Unit tests for [InputLogic.computeSpacingSignals] (#14 spacing policy): the free per-keystroke
14+
* `complete` + `prefixRichScore` signals derived from the suggestion results. Pure logic.
15+
*/
16+
class SpacingSignalsTest {
17+
18+
// mDictType != "user_typed" -> counts as a "real" dictionary source for `complete`.
19+
private val realDict: Dictionary = Dictionary.DICTIONARY_APPLICATION_DEFINED
20+
private val userTyped: Dictionary = Dictionary.DICTIONARY_USER_TYPED
21+
22+
private fun info(word: String, kind: Int, dict: Dictionary): SuggestedWordInfo =
23+
SuggestedWordInfo(word, "", 0, kind, dict,
24+
SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE)
25+
26+
private fun words(typed: SuggestedWordInfo?, typedValid: Boolean,
27+
list: List<SuggestedWordInfo>): SuggestedWords =
28+
SuggestedWords(ArrayList(list), null, typed, typedValid, false, false,
29+
SuggestedWords.INPUT_STYLE_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER)
30+
31+
@Test fun `empty suggestions yield no signals`() {
32+
val s = InputLogic.computeSpacingSignals(SuggestedWords.getEmptyInstance())
33+
assertFalse(s.complete)
34+
assertEquals(0f, s.prefixRichScore, 0f)
35+
}
36+
37+
@Test fun `valid typed word from a real dictionary is complete`() {
38+
val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict)
39+
assertTrue(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete)
40+
}
41+
42+
@Test fun `valid typed word from the user-typed source is NOT complete`() {
43+
val typed = info("xyzzy", SuggestedWordInfo.KIND_TYPED, userTyped)
44+
assertFalse(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete)
45+
}
46+
47+
@Test fun `invalid typed word is not complete`() {
48+
val typed = info("teh", SuggestedWordInfo.KIND_TYPED, realDict)
49+
assertFalse(InputLogic.computeSpacingSignals(words(typed, false, listOf(typed))).complete)
50+
}
51+
52+
@Test fun `prefix-rich score is the fraction of completions`() {
53+
val typed = info("ba", SuggestedWordInfo.KIND_TYPED, realDict)
54+
val list = listOf(
55+
typed,
56+
info("bad", SuggestedWordInfo.KIND_COMPLETION, realDict),
57+
info("bat", SuggestedWordInfo.KIND_COMPLETION, realDict),
58+
info("ball", SuggestedWordInfo.KIND_COMPLETION, realDict),
59+
)
60+
// 3 completions out of 4 candidates.
61+
assertEquals(0.75f, InputLogic.computeSpacingSignals(words(typed, false, list)).prefixRichScore, 1e-6f)
62+
}
63+
64+
@Test fun `no completions yields zero prefix-rich score`() {
65+
val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict)
66+
assertEquals(0f,
67+
InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).prefixRichScore, 0f)
68+
}
69+
}

0 commit comments

Comments
 (0)