refactor(classifier): single-pass hard-drop trigger; cleaner drop reason rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -160,27 +160,17 @@ final class Classifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drop has highest priority (overrides client/unclear/spam/honeypot)
|
// Drop has highest priority (overrides client/unclear/spam/honeypot)
|
||||||
$is_drop_by_rule = self::evaluate_hard_drop($fields);
|
$drop_trigger = self::compute_hard_drop_trigger($fields);
|
||||||
$is_drop_by_score = isset($thresholds['drop']) && $score <= (int)$thresholds['drop'];
|
$is_drop_by_score = isset($thresholds['drop']) && $score <= (int)$thresholds['drop'];
|
||||||
if ($is_drop_by_rule || $is_drop_by_score) {
|
if ($drop_trigger !== null || $is_drop_by_score) {
|
||||||
$label = 'drop';
|
$label = 'drop';
|
||||||
$reasons[] = ['sign' => '×', 'weight' => 0, 'text' => self::drop_reason_label($fields, $thresholds, $is_drop_by_rule)];
|
$reason_text = $drop_trigger ?? ('score ≤ ' . (int)$thresholds['drop']);
|
||||||
|
$reasons[] = ['sign' => '×', 'weight' => 0, 'text' => $reason_text];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['score' => $score, 'label' => $label, 'reasons' => $reasons];
|
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
|
private static function concat_fields(array $fields): string
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
@@ -333,6 +323,40 @@ final class Classifier
|
|||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hard-rule trigger description, or null if no rule fires.
|
||||||
|
* Centralises positive-signal checks + trigger detection so callers don't
|
||||||
|
* recompute. Used by evaluate_hard_drop() and classify()'s reason builder.
|
||||||
|
*
|
||||||
|
* @param array<string,string|string[]> $fields CF7 posted_data
|
||||||
|
* @return string|null e.g. "hard-rule: короткое латинское имя + ≥3 поля-плейсхолдера"
|
||||||
|
*/
|
||||||
|
private static function compute_hard_drop_trigger(array $fields): ?string
|
||||||
|
{
|
||||||
|
$rules = apply_filters('cf7stg_hard_drop_rules', self::DEFAULT_HARD_DROP_RULES);
|
||||||
|
|
||||||
|
// Positive-signal short-circuits
|
||||||
|
$name = self::pick_field($fields, ['your-name', 'name', 'имя', 'fio']);
|
||||||
|
if ($name === '') $name = FieldDetector::find_name($fields);
|
||||||
|
if ($name !== '' && self::is_cyrillic_name($name)) return null;
|
||||||
|
|
||||||
|
$message = self::pick_field($fields, ['your-message', 'message', 'comment', 'сообщение', 'вопрос']);
|
||||||
|
if ($message !== '' && self::is_meaningful_text($message)) return null;
|
||||||
|
|
||||||
|
if (self::has_valid_email_anywhere($fields)) return null;
|
||||||
|
|
||||||
|
// Triggers
|
||||||
|
$hits = [];
|
||||||
|
if (!empty($rules['short_latin_name']) && self::has_short_latin_name($fields)) {
|
||||||
|
$hits[] = 'короткое латинское имя';
|
||||||
|
}
|
||||||
|
if (!empty($rules['placeholder_fields']) && self::count_placeholder_fields($fields) >= 3) {
|
||||||
|
$hits[] = '≥3 поля-плейсхолдера';
|
||||||
|
}
|
||||||
|
if (empty($hits)) return null;
|
||||||
|
return 'hard-rule: ' . implode(' + ', $hits);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard-rule decision: returns true if the submission has zero positive signals
|
* 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
|
* (no cyrillic name, no meaningful text, no valid email anywhere) AND at least
|
||||||
@@ -345,22 +369,6 @@ final class Classifier
|
|||||||
*/
|
*/
|
||||||
public static function evaluate_hard_drop(array $fields): bool
|
public static function evaluate_hard_drop(array $fields): bool
|
||||||
{
|
{
|
||||||
$rules = apply_filters('cf7stg_hard_drop_rules', self::DEFAULT_HARD_DROP_RULES);
|
return self::compute_hard_drop_trigger($fields) !== null;
|
||||||
|
|
||||||
// Любой позитивный сигнал — НЕ 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ final class MessageFormatter
|
|||||||
if (!$reasons) return '—';
|
if (!$reasons) return '—';
|
||||||
$parts = [];
|
$parts = [];
|
||||||
foreach ($reasons as $r) {
|
foreach ($reasons as $r) {
|
||||||
|
// Drop reasons use sign='×' weight=0 as a sentinel — render text only
|
||||||
|
// to avoid the meaningless "×0 hard-rule: ..." prefix.
|
||||||
|
if (($r['sign'] ?? '') === '×' && (int)($r['weight'] ?? 0) === 0) {
|
||||||
|
$parts[] = (string)$r['text'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$parts[] = $r['sign'] . $r['weight'] . ' ' . $r['text'];
|
$parts[] = $r['sign'] . $r['weight'] . ' ' . $r['text'];
|
||||||
}
|
}
|
||||||
return esc_html(implode(', ', $parts));
|
return esc_html(implode(', ', $parts));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ final class ClassifierTest extends Cf7stgTestCase
|
|||||||
self::assertGreaterThanOrEqual(3, $r['score']);
|
self::assertGreaterThanOrEqual(3, $r['score']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_seo_spam_with_url_is_red(): void
|
public function test_seo_spam_with_url_yields_drop(): void
|
||||||
{
|
{
|
||||||
// score = SEO(-2) + URL(-3) + latin_only(-1) = -6, which is ≤ drop threshold(-5)
|
// score = SEO(-2) + URL(-3) + latin_only(-1) = -6, which is ≤ drop threshold(-5)
|
||||||
// → label promoted from spam to drop (score-based drop, no hard-rule since email is valid)
|
// → label promoted from spam to drop (score-based drop, no hard-rule since email is valid)
|
||||||
|
|||||||
Reference in New Issue
Block a user