# checkOverseo Specification

## 1. Цель

`checkOverseo` — локальный инструмент проверки перетошноты, заспамленности, водности и ключевых повторов в SEO-правках.

Это не платная интеграция и не внешний API. Мы используем Text.ru только как референс по смыслу показателей и устройству SEO-анализа текста.

Главный вопрос инструмента:

```text
Текст нормально оптимизирован или уже переблеван ключами?
```

## 2. Референс: что берём из Text.ru как идею

Из Text.ru SEO-анализатора берём не API, а логику показателей:

- количество символов;
- количество слов;
- процент водности;
- процент заспамленности;
- список ключей;
- группы ключей;
- смешанные слова из разных алфавитов;
- подсветка проблемных повторов, если получится реализовать.

Источники для ориентира:

- Text.ru API/check description: https://text.ru/api-check
- Text.ru SEO-анализ: https://text.ru/seo
- Text.ru FAQ SEO-анализ: https://text.ru/faq/instrukcii/seo-analiz-teksta

Важно: Text.ru — только ориентир. Мы не подключаем их API и не платим за него.

## 2.1. Разбор публичной модели Text.ru

По публичному описанию Text.ru SEO-анализатор устроен как набор параллельных проверок текста:

```text
raw text
  |
  +-- character/word counter
  +-- keyword extractor
  +-- keyword group builder
  +-- water detector
  +-- spam detector
  +-- mixed alphabet detector
  +-- highlighter / word position mapper
```

### 2.1.1. Базовые счётчики

Text.ru считает:

```text
count_chars_with_space
count_chars_without_space
count_words
```

Нам нужно повторить это локально на каждом scope:

```text
page
section
title
description
heading
alt
filename
```

### 2.1.2. Ключи

Text.ru описывает поиск поисковых ключей в тексте, количество вхождений и морфологические варианты. В API manual это выражено как:

```text
list_keys:
  key_title
  count
```

Где `count` — количество вхождений ключа во всех формах.

Наша локальная версия:

```text
known_keys:
  - берём из keywords.yml
  - exact phrase
  - soft_forms

detected_keys:
  - считаем частотные unigram/bigram/trigram
  - отбрасываем служебный мусор правилами
  - показываем как "detected", не как утверждённый SEO-ключ
```

Важно: мы не обязаны идеально повторять морфологию Text.ru. Для MVP достаточно:

```text
exact phrase
+ manually/model-defined soft_forms
+ detected repeated phrases
```

### 2.1.3. Группы ключей

Text.ru группирует ключи по составу слов и сортирует по числу значимых слов. В API manual это:

```text
list_keys_group:
  key_title
  count
  sub_keys:
    key_title
    count
```

Пример идеи:

```text
"check text" -> group
  "check"
  "text"
```

Наша локальная версия:

```text
keyword group:
  parent phrase:
    - full phrase
    - single-word subkeys
    - soft_forms
    - repeated close phrases
```

Зачем это нужно:

```text
- видеть, что текст не только повторяет один точный ключ;
- видеть, что одна группа терминов забила секцию;
- ловить ситуацию, когда разные формы одного смысла всё равно создают перетошноту.
```

### 2.1.4. Водность

Text.ru определяет воду как процент стоп-слов, фразеологизмов, оборотов, соединительных слов и фраз, которые не несут смысловой нагрузки.

Публичные пороги:

```text
до 15%      естественное содержание воды
15-30%      превышенное содержание воды
от 30%      высокое содержание воды
```

Наша локальная версия не должна быть обычным stopword-remover. Нам нужен редакторский сигнал:

```text
water_score =
  служебные слова
  + слабые бизнес-фразы
  + общие SEO-обороты
  + абстрактные фразы без объекта
  + предложения без конкретного действия/сущности
```

То есть мы считаем приблизительный процент, но в отчёте показываем не только число, а конкретные причины.

### 2.1.5. Заспамленность

Text.ru описывает заспамленность как количество поисковых ключевых слов в тексте.

Публичные пороги:

```text
до 30%      естественное содержание ключевых слов
30-60%      оптимизированный текст
от 60%      сильно оптимизированный / заспамленный текст
```

Наша локальная версия считает это по двум слоям:

```text
1. known-key spam:
   - primary keywords
   - secondary keywords
   - soft_forms

2. detected-key spam:
   - частотные слова
   - частотные биграммы
   - частотные триграммы
   - группы похожих ключей
```

Для нас важнее не общий процент по странице, а риск по секции:

```text
section spam > page spam
```

Потому что лендинг может быть нормальным целиком, но отдельная карточка будет переблевана ключом.

### 2.1.6. Mixed words

Text.ru ищет слова, где смешаны символы разных алфавитов со схожим написанием.

Наша локальная версия:

```text
mixedWordsDetector:
  - кириллица + латиница внутри одного слова
  - похожие символы: а/a, о/o, е/e, р/p, с/c, х/x
  - выдавать позиции и snippets
```

Это не SEO-ключи, но полезная техническая проверка качества текста.

### 2.1.7. Подсветка и позиции

В Text.ru API есть режимы, где возвращаются:

```text
words
text_view
words_pos
```

Это значит, что их интерфейс может подсвечивать проблемные слова/ключи по позициям.

Наша локальная версия должна с самого начала хранить offsets:

```ts
type Token = {
  text: string;
  normalized: string;
  start: number;
  end: number;
  sentenceIndex: number;
  paragraphIndex: number;
};
```

Зачем:

```text
- подсветить ключ в UI;
- показать proximity;
- показать повтор рядом;
- показать water phrase;
- показать mixed word;
- дать Codex точный snippet для правки.
```

## 3. Архитектура

```text
checkOverseo
  |
  +-- TextNormalizer
  |     +-- normalize spaces
  |     +-- normalize quotes/dashes where needed
  |     +-- lowercase for counting
  |     +-- keep original text for report snippets
  |
  +-- Tokenizer
  |     +-- words
  |     +-- sentences
  |     +-- bigrams
  |     +-- trigrams
  |
  +-- KeywordCounter
  |     +-- exact keywords
  |     +-- soft_forms from keywords.yml
  |     +-- key groups
  |
  +-- NauseaAnalyzer
  |     +-- classic nausea
  |     +-- academic nausea
  |     +-- phrase repetition
  |
  +-- WaterAnalyzer
  |     +-- local water-word dictionary
  |     +-- weak phrase markers
  |     +-- business-cliche markers
  |
  +-- ProximityAnalyzer
  |     +-- same keyword too close
  |     +-- repeated key in neighboring sentences
  |
  +-- ScopeAnalyzer
  |     +-- page
  |     +-- section
  |     +-- title
  |     +-- description
  |     +-- h1/h2
  |     +-- alt
  |     +-- filename
  |
  +-- ReportBuilder
        +-- warnings
        +-- scores
        +-- snippets
        +-- verdict
```

## 4. Вход

```ts
type CheckOverseoInput = {
  text: string;
  originalText?: string;
  scope: "page" | "section" | "title" | "description" | "heading" | "alt" | "filename";
  sourceId: string;
  sectionId?: string;
  keywords: KeywordEntry[];
  pageMap: PageMap;
  options?: CheckOverseoOptions;
};
```

## 5. Источник правил

### 5.1. `keywords.yml`

```yaml
keywords:
  - phrase: "операционная модель бизнеса"
    type: "primary"
    section: "hero"
    exact_limit: 1
    max_section_count: 2
    soft_forms:
      - "операционная модель"
      - "модель бизнеса"
    status: "use"
```

### 5.2. `page-map.yml`

```yaml
page:
  primary_intent: "главный смысл страницы"
  forbidden_intents: []

sections:
  hero:
    meaning: "смысл секции"
    primary_keywords: []
    secondary_keywords: []
    max_lines_hint: 6
```

### 5.3. Local dictionaries

```text
data/rules/
  water-words-ru.yml
  weak-business-phrases.yml
  negative-keyword-intents.yml
  safe-technical-terms.yml
```

Словари нужны не для автоматического переписывания, а для предупреждений.

## 6. Проверки

### 6.1. Counts

```text
- chars_with_space
- chars_without_space
- words_count
- sentences_count
- average_sentence_length
```

### 6.2. Keyword counts

```text
- exact keyword count
- soft_forms count
- primary keyword count
- secondary keyword count
- key group count
- keyword density by page
- keyword density by section
```

### 6.3. Classic nausea

Классическая тошнота считается как корень из максимальной частоты самого повторяемого слова или ключевой фразы.

```text
classic_nausea = sqrt(max_term_frequency)
```

Используется как локальный индикатор частого повторения.

### 6.4. Academic nausea

Академическая тошнота считается как доля самого частого слова/термина в общем количестве слов.

```text
academic_nausea_percent = max_term_frequency / words_count * 100
```

Также считаем отдельно:

```text
keyword_academic_nausea_percent
```

Чтобы видеть не просто частое служебное слово, а именно перебор нужного нам ключа.

### 6.5. Phrase repetition

```text
- repeated exact phrase
- repeated bigram
- repeated trigram
- same sentence starts
- repeated sentence skeleton
```

### 6.6. Proximity

```text
- один и тот же ключ повторяется в соседних предложениях;
- один и тот же ключ повторяется в соседних абзацах;
- primary keyword стоит слишком близко к прошлому вхождению;
- alt повторяет heading без причины;
- filename повторяет alt и ключ без смысла.
```

### 6.7. Water score

Локальная водность — не попытка идеально повторить Text.ru. Это наш сигнал:

```text
water_score = water_words_count / words_count * 100
```

Плюс отдельные предупреждения:

```text
- слабые бизнес-фразы;
- общие SEO-фразы;
- много абстрактных существительных;
- длинные предложения без конкретного объекта;
- фразы, которые не добавляют смысла.
```

### 6.8. Scope rules

Разные зоны проверяются по-разному:

```text
page:
  - общий риск заспамленности
  - общий список ключей

section:
  - соответствие ключей секции
  - локальный перебор
  - конфликт с meaning секции

title/description:
  - нет ли повторения одного ключа дважды
  - нет ли набора ключей вместо нормального сниппета

heading:
  - нет ли keyword stuffing
  - заголовок соответствует секции

alt:
  - описывает изображение
  - ключ встроен естественно
  - нет списка ключей

filename:
  - имя файла осмысленное
  - нет набора ключей через дефисы
  - имя файла не конфликтует с содержанием изображения
```

## 7. Пороговые значения MVP

Пороги должны быть настраиваемыми, потому что B2B/технические тексты неизбежно повторяют термины.

```yaml
thresholds:
  keyword_density:
    section_warning: 3
    section_problem: 5
  exact_keyword:
    default_limit: 2
  classic_nausea:
    warning: 4
    problem: 7
  academic_nausea_percent:
    warning: 7
    problem: 10
  proximity:
    min_sentences_between_same_keyword: 2
  water_percent:
    warning: 20
    problem: 30
```

Пороги не должны автоматически запрещать текст. Они создают предупреждения.

## 8. Report

```ts
type OverSeoReport = {
  scope: "page" | "section" | "title" | "description" | "heading" | "alt" | "filename";
  sourceId: string;
  sectionId?: string;

  counts: {
    charsWithSpace: number;
    charsWithoutSpace: number;
    words: number;
    sentences: number;
  };

  scores: {
    waterPercent?: number;
    classicNausea?: number;
    academicNauseaPercent?: number;
    maxKeywordDensityPercent?: number;
  };

  keys: Array<{
    phrase: string;
    count: number;
    densityPercent: number;
    type?: "primary" | "secondary" | "soft_form" | "detected";
    sectionAllowed?: boolean;
  }>;

  keyGroups: Array<{
    phrase: string;
    count: number;
    subKeys: Array<{
      phrase: string;
      count: number;
    }>;
  }>;

  warnings: Array<{
    severity: "info" | "warning" | "critical";
    code: string;
    message: string;
    phrase?: string;
    count?: number;
    limit?: number;
    snippet?: string;
  }>;

  verdict: {
    status: "ok" | "review" | "problem";
    summary: string;
  };
};
```

## 9. UI

UI должен показывать:

```text
Summary:
  - status
  - words count
  - water score
  - classic nausea
  - academic nausea
  - max keyword density

Keywords:
  - phrase
  - count
  - density
  - allowed section
  - warnings

Warnings:
  - severity
  - reason
  - snippet
  - suggested action

Scopes:
  - page
  - sections
  - title/description
  - headings
  - alt
  - filename
```

## 10. Codex role

Codex не считает метрики вместо инструмента.

Codex используется после отчёта:

```text
1. Объяснить предупреждения человеческим языком.
2. Предложить варианты исправления.
3. Сохранить смысл и тон.
4. Не увеличивать текст без необходимости.
5. Не вставлять ключи, если отчёт показывает перебор.
```

## 11. Главное ограничение

`checkOverseo` не должен становиться тупой машиной запретов.

Он показывает риски:

```text
перебор
слишком близко
слишком часто
слишком водно
слишком похоже на набор ключей
```

Финальное решение принимает пользователь.
