Skip to content

Commit adbd291

Browse files
committed
fix(category): gate weak heuristic blocks before the type tags
A refuting review found the confidence gate on heuristic categories sat after the database-type mapping. HeuristicChecker types its blocks from the same signals, so in aggressive mode a lone 30-point VoIP-range match or 40-point rapid-fire match, typed "robocall", still named the Robocall category and a Robocall rule could decide the call on it. The gate now runs first, and the later heuristic branch no longer repeats it. Test changes: a new CallCategoryResolverTest case covers weak typed heuristic blocks, a strong one, and the same type from the database at low confidence. Letting a typed weak block past the gate fails it.
1 parent b35e561 commit adbd291

3 files changed

Lines changed: 41 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ All notable changes to CallShield will be documented in this file.
4646
campaign range as a robocall, and a toll-free one as a telemarketer, so
4747
the action you picked for that category applies. Those mappings existed
4848
but never matched, because they looked for internal names the block
49-
description had already turned into plain words. They only apply to a
50-
block as confident as the default setting requires, so a lone
51-
neighbor-spoof match that only aggressive mode blocks stays uncategorized
52-
and no category rule can let it through.
49+
description had already turned into plain words. A suspicious-pattern
50+
block only gets a category when it's as confident as the default setting
51+
requires. One that only aggressive mode blocks, such as a lone
52+
neighbor-spoof or VoIP-range match, stays uncategorized, so no category
53+
rule can let it through. Before, a weak match that the block had typed as
54+
a robocall still counted as one.
5355

5456
- CallShield starts faster when a call wakes it. Looking for an interrupted
5557
backup restore held the main thread about 0.7 seconds on every start

‎app/src/main/java/com/sysadmindoc/callshield/data/CallCategory.kt‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ object CallCategoryResolver {
8181
if (!result.isSpam) return CallCategory.Unknown
8282
CategoryCallPolicy.parseMatchSource(result.matchSource)?.let { return it.category }
8383

84+
// A heuristic block names a category only at the score the default
85+
// setting blocks at. Its type comes from the same signals, so this
86+
// goes before the type tags: aggressive mode blocks a lone 30-point
87+
// VoIP-range match typed "robocall", and a lone 50-point neighbor-spoof
88+
// match also fires on local banks and schools. Naming either would let
89+
// a category rule decide the call on that alone.
90+
if (result.reasonCode == BlockReasonCode.HEURISTIC && result.confidence < HEURISTIC_CATEGORY_MIN_CONFIDENCE) {
91+
return CallCategory.Unknown
92+
}
93+
8494
// (1) Database type tags are authoritative — they came from
8595
// labeled community/FCC/FTC data, not heuristics.
8696
when (result.type.lowercase().trim()) {
@@ -120,11 +130,8 @@ object CallCategoryResolver {
120130
// Heuristic reasons: map the strongest signals. A live result carries
121131
// the raw tokens in `signals`; its description is display text in the
122132
// app language (and always had the underscores replaced), so it only
123-
// helps callers that still pass tokens there. Only at the confidence
124-
// the default setting blocks at: aggressive mode blocks on a single
125-
// 50-point neighbor-spoof match, which also fires on local banks and
126-
// schools, and naming that Scam would let a Scam rule decide it.
127-
if (result.reasonCode == BlockReasonCode.HEURISTIC && result.confidence >= HEURISTIC_CATEGORY_MIN_CONFIDENCE) {
133+
// helps callers that still pass tokens there.
134+
if (result.reasonCode == BlockReasonCode.HEURISTIC) {
128135
val evidence = (result.signals + desc).joinToString(" ")
129136
when {
130137
"wangiri" in evidence -> return CallCategory.Wangiri

‎app/src/test/java/com/sysadmindoc/callshield/data/CallCategoryResolverTest.kt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ class CallCategoryResolverTest {
113113
assertEquals(CallCategory.Scam, CallCategoryResolver.resolve(weak.copy(confidence = 60)))
114114
}
115115

116+
@Test
117+
fun `a weak heuristic block's type names no category either`() {
118+
// HeuristicChecker types its blocks from the same signals: a lone
119+
// VoIP-range match (30) or rapid-fire match (40) is typed "robocall",
120+
// and aggressive mode blocks both.
121+
fun heuristic(
122+
type: String,
123+
confidence: Int,
124+
vararg signals: String,
125+
) = BlockResult
126+
.block("heuristic", type = type, description = "", confidence = confidence, signals = signals.toList())
127+
.toSpamCheckResult()
128+
129+
assertEquals(CallCategory.Unknown, CallCategoryResolver.resolve(heuristic("robocall", 30, "voip_spam_range")))
130+
assertEquals(CallCategory.Unknown, CallCategoryResolver.resolve(heuristic("robocall", 40, "rapid_fire")))
131+
assertEquals(CallCategory.Robocall, CallCategoryResolver.resolve(heuristic("robocall", 70, "rapid_fire", "voip_spam_range")))
132+
// The same type from the database is authoritative at any confidence.
133+
assertEquals(
134+
CallCategory.Robocall,
135+
CallCategoryResolver.resolve(SpamCheckResult(isSpam = true, matchSource = "database", type = "robocall", confidence = 30)),
136+
)
137+
}
138+
116139
@Test
117140
fun `heuristic with rapid_fire resolves to Robocall`() {
118141
val result =

0 commit comments

Comments
 (0)