Feedback Arc Set
Feedback Arc Set (FAS) — это набор рёбер для удаления, после которого цикл исчезает. Для клубка из взаимно импортирующихся модулей знать SCC недостаточно — хочется знать, какой именно один-два импорта реально замыкают петлю.
Проблема
Tarjan-SCC говорит: «вот эти 50 модулей в цикле». Это правда — и бесполезно. 50 модулей одновременно не починишь. Хочется: «вот эти 2 импорта; удалите или инвертируйте — все циклы в группе пропадут».
Для произвольных ориентированных графов вычисление минимального feedback-arc-set — NP-hard. Оптимум нам не нужен — нужен маленький разумный набор, быстро.
Эвристика
Для каждого SCC размером ≥ 2:
Скорим каждое внутреннее ребро взвешенной комбинацией:
- fan-out исходного модуля (больше исходящих импортов → больше запутанности),
- coupling-score исходного модуля (композит fan-in × fan-out),
- является ли ребро динамическим (
() => import(...)) — слегка предпочтительнее на разрыв, поскольку часто и так уже lazy.
Coupling доминирует. Идея: удаление ребра из тяжело-связанного модуля скорее обнажит реальную архитектурную ошибку (и дороже окупится при фиксе).
Жадно отбираем рёбра — самое высокое первое — пока в SCC не останется циклов. После каждого удаления перезапускаем SCC-детект, чтобы знать, когда останавливаться. Для типичных SCC (≤ 20 модулей) это сходится сильно меньше миллисекунды.
Ограничиваем поиск. Для очень больших 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 только указывает на ребро. Как именно его убирать — ваше решение.
См. также
- Поиск циклов — как циклы находятся в первую очередь.
- Рекомендации — полный каталог, включая
cycle-break-candidateиcycle-break-cluster.