Skip to content

Feedback Arc Set

Feedback Arc Set (FAS) — это набор рёбер для удаления, после которого цикл исчезает. Для клубка из взаимно импортирующихся модулей знать SCC недостаточно — хочется знать, какой именно один-два импорта реально замыкают петлю.

Проблема

Tarjan-SCC говорит: «вот эти 50 модулей в цикле». Это правда — и бесполезно. 50 модулей одновременно не починишь. Хочется: «вот эти 2 импорта; удалите или инвертируйте — все циклы в группе пропадут».

Для произвольных ориентированных графов вычисление минимального feedback-arc-set — NP-hard. Оптимум нам не нужен — нужен маленький разумный набор, быстро.

Эвристика

Для каждого SCC размером ≥ 2:

  1. Скорим каждое внутреннее ребро взвешенной комбинацией:

    • fan-out исходного модуля (больше исходящих импортов → больше запутанности),
    • coupling-score исходного модуля (композит fan-in × fan-out),
    • является ли ребро динамическим (() => import(...)) — слегка предпочтительнее на разрыв, поскольку часто и так уже lazy.

    Coupling доминирует. Идея: удаление ребра из тяжело-связанного модуля скорее обнажит реальную архитектурную ошибку (и дороже окупится при фиксе).

  2. Жадно отбираем рёбра — самое высокое первое — пока в SCC не останется циклов. После каждого удаления перезапускаем SCC-детект, чтобы знать, когда останавливаться. Для типичных SCC (≤ 20 модулей) это сходится сильно меньше миллисекунды.

  3. Ограничиваем поиск. Для очень больших SCC мы кэпаем кандидатное множество, чтобы эвристика оставалась быстрой. Десктоп показывает «search hit the limit — count is an estimate», когда такое случается; счётчик разорванных циклов — это нижняя оценка.

Что вы видите в UI

У каждого цикла есть секция «Imports that close the cycle» с 1–2 конкретными рёбрами. Клик по ребру прыгает к строке импорта в редакторе. Панель рекомендаций показывает:

Removes 7 of 12 cycles in this group.

Это per-edge counterfactual: «если удалить (или инвертировать) этот один импорт — вот сколько циклов в группе пропадут». Для маленьких групп цифра точная; для больших — нижняя оценка (глубина поиска была ограничена).

Почему не просто «удалите ребро X»?

Иногда правильный фикс — не удаление. Это:

  • Инвертировать направление — потянуть зависимость в другую сторону.
  • Переместить тип в общий модуль — если обе стороны импортируют общий тип друг от друга, реальная общая зависимость — это тип.
  • Перевести в import type — если все использования — типы, это самый дешёвый возможный фикс, и Archora флагает его отдельно как type-only candidate.

Archora только указывает на ребро. Как именно его убирать — ваше решение.

См. также

Выпущено под лицензией BUSL-1.1.