# Методика Repo Health Score (версия 1.6) Документ сгенерирован из активных конфигураций расчёта и рекомендаций: `repohealth/methodology/methodology.yaml` и `repohealth/recommend/rules.yaml`. ## Формула ``` metric_i ∈ [0,1] — нормализованное значение метрики или «нет данных» S_c^0 = 100 · Σ w_i·metric_i / Σ w_i по доступным метрикам категории c cov_c = Σ w_i (доступных) / Σ w_i (всех); категория участвует при cov_c ≥ 0.5 или доказанном отсутствии SourceCraft CI S_c = min(S_c^0 × modifier, cap) если сработали модификатор и ограничение Score = Σ W_c·S_c / Σ W_c по оценённым категориям; S_c уже в шкале 0..100 confidence = Σ W_c (доступных) / Σ W_c (всех) ``` Меньше 3 доступных категорий → пометка «малый охват данных». Пустой репозиторий не оценивается. Расчёт привязан к моменту сбора снимка: повторный расчёт тех же данных и версии методики даёт тот же Score даже позже. Новое сканирование отражает прошедшее время и свежие факты. ## Категории и веса | Категория | Вес | |---|---| | Безопасность | 18 | | Состояние кода и техдолг | 21 | | Активность проекта | 18 | | Документация и практики | 17 | | CI/CD | 13 | | Работа с issues | 13 | ## Метрики ### Безопасность (вес 18) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | Открытые уязвимости, взвешенные по критичности (`security.open_weighted`) | балл риска | 5 | линейно: 30 → 0, 0 → 1 | | Доля исправленных findings за 90 дней (`security.resolved_ratio_90d`) | | 2 | как есть, 0..1 | | Открытые секреты в коде (`security.open_secrets`) | шт. | 3 | ступени: ≥0 → 1.0, ≥1 → 0.0 | | Свежий скан безопасности (≤ 30 дней) (`security.scan_fresh`) | | 1 | да → 1, нет → 0 | ### Состояние кода и техдолг (вес 21) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | Плотность TODO/FIXME/HACK на 1000 строк кода (`code.todo_density`) | на 1000 строк | 3 | линейно: 20 → 0, 1 → 1 | | Доля TODO старше 6 месяцев (`code.todo_old_share`) | | 2 | линейно: 1 → 0, 0 → 1 | | Тесты: наличие и доля тестового кода (`code.tests_ratio`) | кода в тестах | 3 | ступени: ≥0 → 0.0, ≥0.0001 → 0.5, ≥0.05 → 0.75, ≥0.15 → 1.0 | | Зафиксированные зависимости (lockfile) при наличии манифеста (`code.lockfile`) | | 1 | да → 1, нет → 0 | | Доля крупных и бинарных файлов (`code.heavy_files_share`) | | 1 | линейно: 0.3 → 0, 0.02 → 1 | | Линтеры, форматтеры, editorconfig (`code.quality_tooling`) | инструментов | 1 | ступени: ≥0 → 0.0, ≥1 → 0.7, ≥2 → 1.0 | ### Активность проекта (вес 18) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | Давность последнего осмысленного коммита (`activity.days_since_last_commit`) | дней назад | 3 | exp(−дни/90) | | Осмысленные коммиты за 90 дней (`activity.meaningful_commits_90d`) | шт. | 2 | 1 − exp(−x/30) | | Живые авторы за год (без ботов) (`activity.human_authors_365d`) | чел. | 2 | 1 − exp(−x/3) | | Bus factor (авторов, покрывающих 50% коммитов за год) (`activity.bus_factor`) | чел. | 2 | ступени: ≥0 → 0.0, ≥1 → 0.2, ≥2 → 0.6, ≥3 → 1.0 | | Влитые PR за 90 дней (`activity.merged_prs_90d`) | шт. | 1 | 1 − exp(−x/10) | | Разных авторов влитых PR за год (`activity.pr_authors_365d`) | чел. | 1 | 1 − exp(−x/2) | | Давность последнего релиза или тега (`activity.release_recency_days`) | дней назад | 1 | exp(−дни/180) | ### Документация и практики (вес 17) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | README: наличие, объём и ключевые разделы (`docs.readme_quality`) | | 4 | как есть, 0..1 | | Лицензия (распознанный SPDX) (`docs.license`) | | 2 | как есть, 0..1 | | CONTRIBUTING, CODEOWNERS, CHANGELOG, SECURITY.md (`docs.practices`) | из 4 | 2 | линейно: 0 → 0, 3 → 1 | | Описание репозитория заполнено в SourceCraft (`docs.description`) | | 1 | да → 1, нет → 0 | | Правила ревью и политики веток (`docs.sourcecraft_policies`) | из 2 | 2 | ступени: ≥0 → 0.0, ≥1 → 0.6, ≥2 → 1.0 | | Инструкция запуска и сборки (`docs.run_instructions`) | | 2 | да → 1, нет → 0 | ### CI/CD (вес 13) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | Есть конфигурация CI (.sourcecraft/ci.yaml) (`ci.config_present`) | | 3 | да → 1, нет → 0 | | Успешные прогоны среди последних 30 (с весом по свежести) (`ci.success_rate`) | | 4 | как есть, 0..1 | | Последний прогон успешен (`ci.last_status_ok`) | | 2 | да → 1, нет → 0 | | Нестабильность: чередование успех/провал (`ci.flakiness`) | | 2 | линейно: 0.6 → 0, 0.1 → 1 | | Медианная длительность прогона (`ci.duration_median_min`) | мин | 1 | линейно: 30 → 0, 5 → 1 | | CI запускается на pull request (`ci.pr_trigger`) | | 1 | да → 1, нет → 0 | ### Работа с issues (вес 13) | Метрика | Единица | Вес | Нормализация | |---|---|---|---| | Зависшие открытые issues (без обновления > 60 дней) (`issues.stale_share`) | открытых | 3 | линейно: 1 → 0, 0 → 1 | | Медиана первого ответа или текущего ожидания (`issues.first_response_median_h`) | ч | 3 | линейно: 336 → 0, 24 → 1 | | Медианное время закрытия (за 180 дней) (`issues.close_median_days`) | дней | 2 | линейно: 90 → 0, 7 → 1 | | Закрытые / созданные за 90 дней (`issues.backlog_trend`) | | 2 | линейно: 0.5 → 0, 1.2 → 1 | | Использование меток, приоритетов, исполнителей (`issues.triage_usage`) | issues | 1 | как есть, 0..1 | ## Gates (жёсткие ограничения категории) | Условие | Категория | Потолок | |---|---|---| | Открытая critical-уязвимость по данным AppSec | Безопасность | ≤ 25 | | Находка Secret scanning высокой критичности | Безопасность | ≤ 15 | | Последние 5 прогонов CI неуспешны | CI/CD | ≤ 30 | | Нет коммитов больше года | Активность проекта | ≤ 20 | | README отсутствует | Документация и практики | ≤ 40 | ## Модификаторы (антинакрутка) - **Признаки искусственной активности: пустые, залповые или шаблонные коммиты** — категория «Активность проекта» умножается на `activity.authenticity`, флаг `artificial_activity`. Условие: `raw.get('activity.authenticity') is not None and raw['activity.authenticity'] < 0.5`. ## Правила «нет данных» - Метрика без данных исключается из расчёта, её вес перераспределяется на доступные метрики категории. - Категория с покрытием ниже порога получает статус «Нет данных» и не участвует в Score; охват данных (confidence) снижается. - Подтверждённое отсутствие признака оценивается только когда признак применим: LICENSE учитывается у открытых репозиториев, а у приватных исключается как неприменимая метрика. - Подтверждённое полным деревом отсутствие `.sourcecraft/ci.yaml` даёт CI/CD = 0 даже при закрытой истории запусков. Метрики запусков не выдумываются и остаются без данных. - История CI старше 90 дней не описывает текущий CI и не запускает ограничение за пять неуспешных прогонов. - Ноль issues или PR в репозитории → соответствующие метрики «нет данных», а не плохой результат. - Усечённые списки issues и PR не используются для долей и точных счётчиков; частичный просмотр кода не доказывает отсутствие тестов. - Ошибка чтения существующего README не считается пустым README; показатель остаётся без данных. - Для issues без ответа дольше недели учитываем уже прошедшее время ожидания. Это нижняя граница, а не выдуманная дата будущего ответа. - Загруженный результат AppSec учитывается при подтверждённой дате сканирования не старше 90 дней. Время загрузки файла не считается датой скана. Открытые находки SourceCraft для текущего коммита учитываются и без даты скана; пустой или устаревший импорт не доказывает отсутствие уязвимостей. - Пользовательская выгрузка SARIF используется только в личном анализе; сервис не подтверждает её происхождение. Для автоматического сбора используются реальные находки SourceCraft AppSec, а балл категории и общий Score рассчитывает Repo Health. - Счётчик комментариев AppSec в PR не раскрывает статус и критичность finding и не входит в Score. SBOM без сведений об уязвимостях не завышает Security. ## Рекомендации Каждое правило срабатывает по условию над метриками и ссылается на факты. Рекомендация проверить скан на новом коммите может появиться и при категории без данных; она не утверждает, что прежние находки актуальны. **Ожидаемый эффект** вычисляется пересчётом what-if только если набор оценённых категорий не меняется. Когда чистый скан AppSec не подтверждён или для исправления нужны ещё не полученные данные (например, прогоны нового CI), дельта не показывается. Приоритет = вес критичности ({'critical': 4, 'high': 3, 'medium': 2, 'low': 1}) × max(эффект, 0.5) / трудоёмкость ({'low': 1, 'medium': 1.5, 'high': 2.5}); сработавшие gates всегда наверху. | Правило | Категория | Критичность | Условие | |---|---|---|---| | Проверить raw[security.open_critical] находок AppSec критической важности (`sec.critical_open`) | Безопасность | critical | `raw.get('security.open_critical', 0) > 0` | | Разобрать raw[security.open_noncritical_nonsecret] открытых находок AppSec (`sec.appsec_open`) | Безопасность | medium / high при high-находках | `raw.get('security.open_noncritical_nonsecret', 0) > 0` | | Проверить возможный секрет в репозитории (`sec.secret_open`) | Безопасность | medium | `raw.get('security.open_secrets', 0) > 0 and raw.get('security.open_secrets_high', 0) == 0` | | Проверить находку Secret scanning высокой критичности (`sec.secret_high`) | Безопасность | critical | `raw.get('security.open_secrets_high', 0) > 0` | | Проверить AppSec для текущего коммита (`sec.refresh_scan`) | Безопасность | low | `raw.get('security.current_head_unverified') is True` | | Настроить CI: добавить .sourcecraft/ci.yaml (`ci.no_config`) | CI/CD | high | `raw.get('ci.config_present') is False` | | Починить падающий CI (`ci.failing`) | CI/CD | high | `raw.get('ci.last_status_ok') is False` | | Разобраться с ошибками CI (`ci.unstable`) | CI/CD | medium | `raw.get('ci.success_rate') is not None and raw['ci.success_rate'] < 0.7 and raw.get('ci.last_status_ok') is not False and raw.get('ci.recent_failures', 1) > 0` | | Запускать CI на pull request (`ci.no_pr_trigger`) | CI/CD | low | `raw.get('ci.config_present') is True and raw.get('ci.pr_trigger') is False` | | Добавить файл LICENSE (`docs.no_license`) | Документация и практики | low | `raw.get('docs.license') == 0` | | Использовать стандартный текст лицензии (`docs.license_unknown`) | Документация и практики | low | `raw.get('docs.license') == 0.7` | | Написать README (`docs.readme_missing`) | Документация и практики | high | `raw.get('docs.readme_length') == 0` | | Дополнить короткий README (`docs.readme_short`) | Документация и практики | medium | `raw.get('docs.readme_length') is not None and raw['docs.readme_length'] > 0 and raw['docs.readme_length'] < 200` | | Дополнить README недостающими разделами (`docs.readme_sections`) | Документация и практики | medium | `raw.get('docs.readme_length') is not None and raw['docs.readme_length'] >= 200 and raw.get('docs.readme_quality') is not None and raw['docs.readme_quality'] < 0.7` | | Добавить инструкцию запуска и сборки (`docs.run_instructions`) | Документация и практики | medium | `raw.get('docs.run_instructions') is False and raw.get('docs.readme_length') is not None and raw['docs.readme_length'] >= 200` | | Добавить CONTRIBUTING и CODEOWNERS (`docs.practices`) | Документация и практики | low | `raw.get('docs.practices') is not None and raw['docs.practices'] == 0 and raw.get('activity.human_authors_365d', 0) >= 2` | | Включить правила ревью и защиту веток SourceCraft (`docs.policies`) | Документация и практики | low | `raw.get('docs.sourcecraft_policies') == 0 and raw.get('activity.merged_prs_90d', 0) >= 3` | | Заполнить описание репозитория (`docs.description`) | Документация и практики | low | `raw.get('docs.description') is False` | | Разобрать накопленные TODO/FIXME (raw[code.todo_total_int] шт.) (`code.todo_heavy`) | Состояние кода и техдолг | medium | `raw.get('code.todo_density') is not None and raw['code.todo_density'] > 5` | | Закрыть устаревшие TODO (`code.todo_old`) | Состояние кода и техдолг | low | `raw.get('code.todo_old_share') is not None and raw['code.todo_old_share'] > 0.5 and raw.get('code.todo_density', 0) <= 5` | | Добавить автоматические тесты (`code.no_tests`) | Состояние кода и техдолг | high | `raw.get('code.tests_ratio') is not None and raw['code.tests_ratio'] == 0` | | Зафиксировать версии зависимостей (`code.lockfile`) | Состояние кода и техдолг | low | `raw.get('code.lockfile') is False` | | Подключить линтер и форматтер (`code.tooling`) | Состояние кода и техдолг | low | `raw.get('code.linter_count') == 0 and raw.get('code.todo_density') is not None` | | Проект не обновлялся больше года (`activity.dead`) | Активность проекта | high | `raw.get('activity.last_commit_confirmed') is True and raw.get('activity.days_since_last_commit') is not None and raw['activity.days_since_last_commit'] > 365` | | Прояснить статус поддержки проекта (`activity.maintenance_status`) | Активность проекта | low | `raw.get('activity.last_commit_confirmed') is True and raw.get('activity.days_since_last_commit') is not None and raw['activity.days_since_last_commit'] > 90 and raw['activity.days_since_last_commit'] <= 365 and raw.get('activity.meaningful_commits_90d') == 0` | | Снизить зависимость от одного разработчика (`activity.bus_factor`) | Активность проекта | medium | `raw.get('activity.bus_factor') == 1 and raw.get('activity.meaningful_commits_90d', 0) > 0` | | Убрать пустые и залповые коммиты (`activity.artificial`) | Активность проекта | medium | `raw.get('activity.authenticity') is not None and raw['activity.authenticity'] < 0.5` | | Публиковать релизы (`activity.no_releases`) | Активность проекта | low | `raw.get('activity.no_releases_confirmed') is True and raw.get('activity.meaningful_commits_90d', 0) >= 10` | | Разобрать зависшие issues (`issues.stale`) | Работа с issues | medium | `raw.get('issues.stale_share') is not None and raw['issues.stale_share'] > 0.3` | | Отвечать в issues быстрее (`issues.slow_response`) | Работа с issues | medium | `raw.get('issues.first_response_median_h') is not None and raw['issues.first_response_median_h'] > 72` | | Ввести триаж issues: метки, приоритеты, исполнители (`issues.triage`) | Работа с issues | low | `raw.get('issues.triage_usage') is not None and raw['issues.triage_usage'] < 0.3` |