feat(classifier): label 'drop' via hard-rule + score threshold (-5 default)
- Add DEFAULT_THRESHOLDS['drop'] = -5 and DEFAULT_HARD_DROP_RULES const
- Add evaluate_hard_drop(): fires when no positive signals + short latin name OR ≥3 placeholder fields
- Wire into classify(): drop overrides all other labels; synthetic reason explains trigger
- Add drop_reason_label() and 'drop' => '⛔ авто-дроп' to MessageFormatter::LABEL_MAP
- Update ClassifierTest/MessageFormatterTest to extend Cf7stgTestCase (filter isolation)
- Adjust pre-existing assertions that now correctly yield 'drop' instead of 'spam'
This commit is contained in:
@@ -20,6 +20,12 @@ final class Classifier
|
||||
public const DEFAULT_THRESHOLDS = [
|
||||
'client' => 3,
|
||||
'spam' => -2,
|
||||
'drop' => -5,
|
||||
];
|
||||
|
||||
public const DEFAULT_HARD_DROP_RULES = [
|
||||
'short_latin_name' => true,
|
||||
'placeholder_fields' => true,
|
||||
];
|
||||
|
||||
public const DEFAULT_KEYWORDS = [
|
||||
@@ -153,9 +159,28 @@ final class Classifier
|
||||
$label = 'spam';
|
||||
}
|
||||
|
||||
// Drop has highest priority (overrides client/unclear/spam/honeypot)
|
||||
$is_drop_by_rule = self::evaluate_hard_drop($fields);
|
||||
$is_drop_by_score = isset($thresholds['drop']) && $score <= (int)$thresholds['drop'];
|
||||
if ($is_drop_by_rule || $is_drop_by_score) {
|
||||
$label = 'drop';
|
||||
$reasons[] = ['sign' => '×', 'weight' => 0, 'text' => self::drop_reason_label($fields, $thresholds, $is_drop_by_rule)];
|
||||
}
|
||||
|
||||
return ['score' => $score, 'label' => $label, 'reasons' => $reasons];
|
||||
}
|
||||
|
||||
private static function drop_reason_label(array $fields, array $thresholds, bool $by_rule): string
|
||||
{
|
||||
if ($by_rule) {
|
||||
$hits = [];
|
||||
if (self::has_short_latin_name($fields)) $hits[] = 'короткое латинское имя';
|
||||
if (self::count_placeholder_fields($fields) >= 3) $hits[] = '≥3 поля-плейсхолдера';
|
||||
return 'hard-rule: ' . implode(' + ', $hits);
|
||||
}
|
||||
return 'score ≤ ' . (int)$thresholds['drop'];
|
||||
}
|
||||
|
||||
private static function concat_fields(array $fields): string
|
||||
{
|
||||
$out = [];
|
||||
@@ -307,4 +332,35 @@ final class Classifier
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard-rule decision: returns true if the submission has zero positive signals
|
||||
* (no cyrillic name, no meaningful text, no valid email anywhere) AND at least
|
||||
* one trigger fires (short latin name OR ≥3 placeholder fields).
|
||||
*
|
||||
* Triggers can be disabled individually via filter `cf7stg_hard_drop_rules`.
|
||||
*
|
||||
* @param array<string,string|string[]> $fields CF7 posted_data
|
||||
* @return bool True if the submission should be silently dropped by hard-rule.
|
||||
*/
|
||||
public static function evaluate_hard_drop(array $fields): bool
|
||||
{
|
||||
$rules = apply_filters('cf7stg_hard_drop_rules', self::DEFAULT_HARD_DROP_RULES);
|
||||
|
||||
// Любой позитивный сигнал — НЕ drop
|
||||
$name = self::pick_field($fields, ['your-name', 'name', 'имя', 'fio']);
|
||||
if ($name === '') $name = FieldDetector::find_name($fields);
|
||||
if ($name !== '' && self::is_cyrillic_name($name)) return false;
|
||||
|
||||
$message = self::pick_field($fields, ['your-message', 'message', 'comment', 'сообщение', 'вопрос']);
|
||||
if ($message !== '' && self::is_meaningful_text($message)) return false;
|
||||
|
||||
if (self::has_valid_email_anywhere($fields)) return false;
|
||||
|
||||
// Триггеры
|
||||
if (!empty($rules['short_latin_name']) && self::has_short_latin_name($fields)) return true;
|
||||
if (!empty($rules['placeholder_fields']) && self::count_placeholder_fields($fields) >= 3) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ final class MessageFormatter
|
||||
'client' => ['emoji' => '🟢', 'text' => 'похоже клиент'],
|
||||
'unclear' => ['emoji' => '🟡', 'text' => 'неясно'],
|
||||
'spam' => ['emoji' => '🔴', 'text' => 'похоже спам'],
|
||||
'drop' => ['emoji' => '⛔', 'text' => 'авто-дроп'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user