1. Migration discovery как защита от хаоса
Current-state assessment легко принять за бумажную разминку перед «настоящей работой». Но в миграциях настоящая работа как раз и начинается с того, что вы перестаёте гадать. Не с правок в build.gradle, а с точного ответа — из какой технической точки проект стартует прямо сейчас.
Когда вы мигрируете проект, вы меняете не только код. Вы меняете договорённость между кодом, сборкой, рантаймом, библиотеками, конфигами, тестами и CI. Не зафиксируете её до первого upgrade — потом не скажете честно, что сломала миграция, а что ломалось и раньше. Такое бывает пугающе часто: команда обновляет Spring Boot, а через час выясняется, что часть тестов неделю красная, просто все успели философски привыкнуть.
Полезно думать о migration discovery как об инвентаризации перед переездом офиса. Скажете «столы есть, компьютеры вроде тоже, грузим» — в день переезда серверная стойка не пройдёт в лифт, принтер окажется на древнем переходнике, а ключ от архива будет у человека в отпуске. В проекте роль этих сюрпризов играют Gradle Wrapper, несовместимые плагины, скрытые transitive dependencies, старые API и конфиги, про которые вспоминают, только когда они начинают гореть.
Поэтому вопрос discovery не «как бы красиво обновиться», а более приземлённый — что у нас есть на самом деле. Первый профессиональный фильтр против хаоса.
2. Migration Current State vs обычный inventory
На этом этапе многие пытаются переиспользовать старый CODEBASE_INVENTORY.md: «карта проекта же есть». Формально да, практически мало. Inventory и migration current state отвечают на разные вопросы — путать их так же полезно, как путать паспорт и медицинскую карту.
| Что сравниваем | |
Migration Current State |
|---|---|---|
| Главный фокус | Архитектура, модули, entry points, потоки выполнения | Версии, build, runtime, конфиги, CI, deprecated API |
| Основные источники | Код, тесты, роуты, структура каталогов | build.gradle, wrapper, docker-compose.yml, CI, application.yml, вывод команд |
| Главный вопрос | «Как устроен проект?» | «Из какой технической точки мы стартуем?» |
| Итоговый смысл | Навигация по codebase | Основа для планирования миграции |
Хорошая новость в том, что один документ не отменяет другой — наоборот, они хорошо работают в паре. Старый inventory даёт карту территории. Current state — температуру воздуха, состояние моста и список дорог, закрытых на ремонт.
3. Evidence для CashFlow Dashboard
Дальше — самая полезная часть. Чтобы inventory был не «ощущением проекта», а опорой, собирайте evidence из конкретных файлов и команд. В CashFlow Dashboard интересен не абстрактный «legacy-репозиторий», а всё, что меняет поведение при смене стека.
Сначала почти всегда смотрят в build.gradle — там большая часть правды про framework, плагины и direct dependencies:
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.18' // Текущая линия Boot
}
sourceCompatibility = '1.8' // Проект реально живёт на Java 8
Даже этот крошечный фрагмент уже даёт два стартовых условия: Spring Boot 2.7.18 и Java 8. Не зафиксируете — будете обсуждать Boot 3.x, не заметив, что он в принципе не дружит с Java 8.
Следом почти всегда открывают wrapper, потому что версия Gradle — не декоративная мелочь, а часть среды сборки.
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip
# Wrapper фиксирует текущую версию Gradle для всей команды
Этот файл особенно полезен тем, что отрезает соблазн «локально стоит новый Gradle, значит проект почти современный». Source of truth — wrapper, а не настроение вашей машины.
Дальше нужен взгляд на инфраструктурные файлы, даже если вы пока не собираетесь «мигрировать инфраструктуру». docker-compose.yml сразу показывает database baseline:
services:
db:
image: postgres:12 # Текущая база проекта
ports:
- "5432:5432"
Это важно не только из-за версии PostgreSQL. Часть поведения бывает привязана к старому диалекту БД, к сериализации времени, к старым индексам. На discovery это не чинят — признают частью текущей среды.
Очень полезный источник — CI: он часто показывает то, что локальная машина разработчика стыдливо скрывает, — другую JDK, другой шаг сборки, другой профиль запуска.
- name: Запуск тестов
run: ./gradlew clean test # Команда, которую проект реально считает baseline
Если в CI одна версия Java, а локально все запускают другую, это уже не просто техническая деталь, а будущий источник миграционных призраков. Ловите их в документе, а не ночью после upgrade.
Наконец, не забывайте про конфиги приложения. Даже небольшой кусок application.yml бывает важнее большого сервиса, если свойства менялись между major-версиями:
spring:
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false # Текущее поведение дат
Такие параметры особенно неприятны тем, что долго выглядят безобидно, а потом внезапно меняют поведение API или тестов. Поэтому assessment — не только про зависимости, но и про то, как проект живёт на настройках.
4. Direct и transitive dependencies проекта
«У нас немного зависимостей» говорят, глядя только на dependencies в build-файле. Это как «я переезжаю налегке», глядя на чемодан и игнорируя антресоль, гараж и пять коробок с проводами «на всякий случай». В миграциях эти коробки называются transitive dependencies.
Direct dependency вы записали в build.gradle сами. Transitive она тащит за собой. На практике именно transitives часто устраивают вечеринку с разбитыми стёклами: вы о них не думали, а они давно живут в проекте как полноправные жильцы.
Чтобы увидеть реальную картину, мало читать build-файл — нужно ещё сохранять вывод команд как evidence:
./gradlew -version > evidence/gradle-version.txt # Фиксируем реальные Gradle и Java
./gradlew dependencies > evidence/dependencies.txt # Сохраняем дерево зависимостей
./gradlew test > evidence/test-baseline.txt # Видим базовое состояние тестов
Здесь важно не то, что команды красиво выглядят. Важно, что эфемерное состояние терминала превращается в файлы, к которым можно вернуться. Особенно это полезно, если вы работаете с Claude Code: ему проще анализировать сохранённый вывод, чем гадать, что было в консоли три сообщения назад.
Типичный фрагмент дерева зависимостей:
+--- org.springframework.boot:spring-boot-starter-web
| \--- org.springframework:spring-web:5.3.x
\--- org.springframework.boot:spring-boot-starter-data-jpa
\--- org.hibernate:hibernate-core:5.6.x
Даже этот короткий кусок уже показывает, что за одним стартёром — целая толпа библиотек. Вы обновляете не «один Boot», а букет: Spring Framework, Hibernate, Jackson, Security и всё прилипшее. Поэтому assessment без дерева рассказывает, кого вы пригласили сами, но молчит о гостях, пришедших плюс-один.
5. Known failures, deprecated APIs и unknowns
Готовясь к миграции, хочется писать только «чистые» факты — версии, файлы, команды. А неприятное (падающие тесты, устаревшие API, странные самописные адаптеры) мозг предлагает не замечать. Именно оно — самая ценная часть.
Начать полезно с known failures. Если тест уже падает до миграции, это не повод прятать его под ковёр — это повод записать его в baseline.
## Известные сбои
- SubscriptionIT.testProrationAtPeriodSwitch падает и на текущем стеке
- BillingReportIT нестабилен при локальном часовом поясе UTC+3
Такой кусок выглядит немного обидно, зато делает документ честным: он экономит часы споров «это миграция сломала» — «нет, оно и раньше было странным».
Отдельно стоит фиксировать deprecated API и старые точки расширения. В CashFlow Dashboard типичные кандидаты — старый Spring Security и javax.*:
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Старый способ конфигурации security
}
И второй классический сигнал:
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Subscription {
@Id
private Long id;
}
Немедленно чинить не нужно — нужен честный статус: здесь точка риска на старой линии фреймворка, её учитываем как часть current state.
И, наконец, самое ценное — секция Unknowns. Новички часто думают, что unknown — это признание слабости. На деле — признание реальности. Не уверены, как поведёт себя кастомный JSON-адаптер, старый платёжный SDK или самописный Hibernate type на новом стеке — не прячьте, записывайте.
## Неизвестное
- Совместимость кастомного JsonAdapter с новой линией Jackson не подтверждена
- Поведение timezone-логики в monthly reports требует ручной проверки
- Реакция старого payment SDK на Java 21 пока не проверена
Хороший документ не делает вид, что команда знает всё: он отделяет подтверждённое от предположительного — потому на него и можно опираться.
6. Claude Code как аналитик, не апгрейдер
Claude Code здесь очень полезен — если правильно задать роль. Не задать — он с энтузиазмом начнёт предлагать обновления, рецепты миграции, рефакторинг и всю ту активность, от которой мы сегодня держимся подальше. На discovery это не исполнитель, а аналитик с хорошей памятью и терпением к скучным документам.
Хорошая инструкция на этот шаг:
Собери current-state inventory для репозитория.
Источники: build.gradle, gradle-wrapper.properties, docker-compose.yml,
.github/workflows/*, application.yml, src/main/**.
Ничего не меняй. Не предлагай upgrade.
Каждое утверждение подтверждай file path или выводом команды.
Всё неподтверждённое выноси в секцию Unknowns.
Если у вас уже есть проектный CLAUDE.md, туда полезно добавить короткий режим именно под migration discovery:
## Режим migration discovery
- Работать только в read-only режиме
- Версии подтверждать file path или выводом команды
- Не предлагать update dependencies без явного запроса
- Всё сомнительное писать в Unknowns
Схема процесса простая:
flowchart LR
A[Файлы репозитория] --> C[Черновик inventory]
B[Вывод команд] --> C
C --> D[Проверка человеком]
D --> E[Migration Current State]
Обратите внимание на последнюю стрелку. Claude не выпускает документ в прод из своей головы: готовит черновик, собирает evidence, помогает ничего не забыть — но «да, это confirmed» остаётся за человеком. И это правильный баланс: AI ускоряет анализ, а не подменяет инженерное решение.
7. Сборка документа Migration Current State
Когда evidence собраны, важно не оставить их жить разрозненно: кусок в терминале, кусок в голове, кусок в переписке. Соберите в один артефакт — не красивый, а рабочий. Держите рядом с другими migration-материалами, в docs/migrations/current-state.md.
Минимальный каркас:
# Текущее состояние миграции — CashFlow Dashboard
## Текущие версии
## Инструменты сборки и runtime
## Прямые и транзитивные зависимости
## CI-команды
## Устаревшие API
## Известные сбои
## Неизвестное
## Заметки о доказательствах
А небольшой заполненный фрагмент — так:
## Текущие версии
- Java: 8
- Spring Boot: 2.7.18
- Gradle: 7.6.4
- PostgreSQL: 12
## Устаревшие API
- WebSecurityConfigurerAdapter в SecurityConfig.java
- javax.persistence.* в entity-класcах
## Неизвестное
- Совместимость payment SDK с Java 21 не подтверждена
Документ не обязан быть литературным — чем суше, тем лучше. В миграциях много полезного выглядит скучно, и это нормально: скука здесь — признак того, что вы перестали фантазировать и начали работать с реальным проектом.
И в какой-то момент вы замечаете очень приятную вещь. После такого assessment проект перестаёт выглядеть чёрным ящиком с надписью «legacy» — он становится набором конкретных фактов, ограничений и неизвестных. А с конкретными фактами уже можно разговаривать без паники — даже если впереди Spring Boot major upgrade, старая база и код, который писал человек с очень богатой внутренней жизнью.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ