1. BEHAVIOR_INVENTORY.md рядом с risk map
Когда у вас уже есть карта рисков, очень легко попасть в ловушку ложного спокойствия: вот опасные зоны, no-test area, fragile config — кажется, будто всё уже понятно. Но risk map говорит, где менять опасно, и молчит о том, какое поведение там живёт и что нельзя сломать. Для изменения кода этого недостаточно.
Удобно держать в голове простое разделение ролей между артефактами:
| Артефакт | На какой вопрос отвечает | Что даёт вам на практике |
|---|---|---|
|
Как система устроена сейчас? | Current-state картину без украшательств |
|
Где изменение особенно опасно и почему? | Приоритет зон внимания и тип риска |
|
Какие сценарии система реально выполняет? | Перечень поведений, которые надо понимать |
| Список characterization candidates | Что стоит зафиксировать тестами до изменений? | Очередь на будущую safety net |
Characterization candidate — это сценарий, который позже имеет смысл зафиксировать baseline-тестом. Сегодня test не пишу и не превращаю candidate в красивую метку важности: ставлю yes или no по риску, неизвестности и missing checks.
Если у вас уже есть CRITICAL_FLOWS.md, он остаётся крупнозернистой картой: где проходят деньги, retry, MRR, возвраты. BEHAVIOR_INVENTORY.md идёт глубже — раскладывает каждый поток на записи уровня сценария: inputs, outputs, evidence, characterization.
Если совсем по-простому, RISK_MAP.md — это карта минного поля, а BEHAVIOR_INVENTORY.md — список того, что в поле лежит. Без второго вы знаете, что шагать опасно, но не понимаете, за что боретесь. Это как прийти в старую квартиру перед ремонтом, обвести красным несущие стены — и не сфотографировать, где были трубы, розетки и выключатели. Ремонт бодрый, а удивление ещё бодрее.
Для CashFlow Dashboard это особенно важно, потому что почти все неприятные истории живут в сценариях, а не в названиях модулей. Не «модуль mrr-engine опасный», а «пауза подписки не уменьшает MRR». Не «payments/ выглядит сложно», а «успешный retry после failed payment, возможно, не создаёт churn dip». Вы мыслите бизнес-сценариями, а не папками, — это и есть переход от чтения кода к пониманию системы.
2. Состав одной записи в BEHAVIOR_INVENTORY.md
Хороший inventory не пытается быть энциклопедией: он подробен настолько, чтобы потом на его основе принять решение, и компактен настолько, чтобы его вообще можно было читать без обезболивающего. Запись строится вокруг одного понятного flow, а не файла, пакета или строки кода, и называется живым действием. Не MrrFormulas.java:88, а «Пауза подписки и последующее возобновление». Не RefundService, а «Частичный refund в середине периода». Тогда её поймёт и инженер, который откроет файл через месяц.
Пример одной записи:
## Поток
Пауза подписки и последующее возобновление
## Текущее поведение
Во время паузы подписка остаётся в active MRR; после resume сумма не меняется
## Входы
plan=pro, $50/month, pause day 10, resume day 40
## Выходы
MRR snapshot остаётся $50 на всём интервале pause/resume
## Существующие тесты
Нет
## Недостающие проверки
Нет проверки MRR во время паузы и после resume
## Кандидат на characterization
yes
## Доказательства
`MrrFormulas.java:88`, `SubscriptionService.java:154`
Внутри такой записи у каждого поля своя задача. Current behavior — что система делает сегодня, без фантазии «как должно быть». Inputs нужны не для красоты, а чтобы сценарий воспроизводился: реальные значения из sample data, логов или тестов, а не «какая-то подписка». Outputs — результат наблюдаемый, а не предполагаемый.
Два поля студенты часто смешивают, и зря: Existing tests («что уже есть») и Missing checks («чего нет, даже если что-то частично покрыто»). Это разные вещи. Тест на happy path не значит, что edge case защищён. В legacy это болезненно: видят один integration test и радостно говорят «ну тесты есть». Есть. Но не про то.
Наконец, поле Characterization candidate — это уже не описание, а инженерное решение: фиксировать ли flow тестом. И очень важно, что появляется оно внутри записи, а не фантазией сбоку — растёт из конкретного flow, а не из общего ощущения тревоги.
3. Текущее поведение не равно правильному поведению
Вот здесь legacy особенно любит подставить. Когда вы описываете поведение системы, почти автоматически хочется «исправить» его прямо в тексте — документация говорит одно, здравый смысл протестует, и рука пишет не то, что код делает, а то, что он должен был бы делать в приличном обществе. Так делать нельзя.
Представьте ситуацию: в BILLING_RULES.md написано, что paused subscription не входит в active MRR. Красиво, логично, бухгалтер плакал бы от счастья. А в коде MrrFormulas.java вы видите, что paused подписка остаётся в active set. Что писать? Поведение по коду плюс пометку о расхождении с документацией. Иначе вы не документируете систему, а спорите с ней. Держите в голове различие:
| Неправильная запись | Правильная запись |
|---|---|
| Во время паузы подписка не должна учитываться в MRR | По текущему коду paused subscription остаётся в active MRR; документация этому противоречит |
| Retry-success корректно восстанавливает доход | После retry-success churn dip не обнаружен; подтверждающего теста нет, нужна ручная проверка |
| Refund считается правильно | По сценарию partial refund середины периода итоговая сумма уменьшается по формуле из RefundService, но поведение на proration не покрыто тестом |
То есть вы всегда описываете сначала факт, а потом, если нужно, расхождение — не наоборот. Это защищает от неприятной истории: команда начинает изменения, уверенная, что зафиксировала baseline, а он уже был тихо «улучшен» на бумаге.
Здесь же важно разделять confirmed fact и assumption. Если вы видите код, но не можете уверенно сказать, что ветка исполняется, — так и пишите: «предположение, нужна ручная проверка». Legacy не становится лучше от уверенного тона. Он просто получает повод сломать вам субботу.
4. Признаки сильного characterization-кандидата
Самая соблазнительная ошибка на этом этапе — поставить yes почти везде: кажется, что чем больше кандидатов, тем безопаснее. На практике это ровно тот случай, когда «много» не значит «хорошо»: если кандидатом становится всё подряд, кандидатом не становится ничего.
Хороший кандидат обычно отвечает хотя бы одному из трёх сильных признаков. Первый — завязан на business-critical или high-risk area из RISK_MAP.md. Второй — edge case без проверок. Третий — расхождение между кодом, документацией и ожиданиями. Сходятся вместе — почти наверняка yes.
| Flow | Решение | Почему |
|---|---|---|
| Пауза подписки и resume | yes | Business-critical, нет тестов, есть расхождение code vs docs |
| Partial refund mid-period | yes | Денежный сценарий, сложная формула, edge case без покрытия |
| Failed payment → retry success | yes | Влияет на churn/MRR, поведение неочевидно, тестов мало |
| Порядок колонок в CSV-экспорте | скорее no | Неприятно, но низкий риск и легко проверяется вручную |
| Текст подсказки на help-экране | no | Не влияет на расчётную логику и не требует characterization baseline |
Для CashFlow Dashboard разумный ориентир — не гнать пятьдесят кандидатов за один заход. Лучше соберите 10–15 сильных сценариев, где велик риск повредить важное поведение: pause/resume, refund, upgrade/downgrade в середине периода, retry logic, переключение плана в день биллинга. Этого хватит, чтобы строить safety net осмысленно.
Если вы используете Claude Code как помощника на этом шаге, лучше давать ему задачу не «придумай тесты», а именно «классифицируй кандидатов». Например:
Для каждой записи из BEHAVIOR_INVENTORY.md реши,
нужен ли characterization candidate = yes.
Ставь yes, если выполняется хотя бы одно условие:
1. сценарий связан с high-risk или business-critical зоной из RISK_MAP.md;
2. есть missing checks для edge case;
3. есть расхождение между кодом и документацией.
Верни: flow, решение, причина, evidence.
Код тестов не предлагай.
Последняя строка здесь особенно важна. Как только вы не запрещаете писать тесты, Claude очень быстро и с энтузиазмом переселяется на следующий шаг. Сегодня вы не строите страховочную сетку, а честно отмечаете, где она вообще понадобится.
5. Связка discovery, risk map и inventory в цепочку
Очень полезно смотреть на этот уровень не как на пять отдельных разговоров, а как на одну цепочку артефактов — тогда BEHAVIOR_INVENTORY.md не «ещё один документ», а звено, вырастающее из evidence.
файлы + тесты + логи + команды
↓
discovery findings + signal log
↓
ARCHITECTURE_CURRENT.md
(при необходимости — MODULE_INVENTORY/CRITICAL_FLOWS)
↓
RISK_MAP.md
↓
BEHAVIOR_INVENTORY.md
↓
characterization candidates (yes/no)
То есть вы не садитесь и не выдумываете flows с нуля — порядок в схеме читается сверху вниз, а решение о фиксации приходит последним. Возьмём один живой пример: discovery показал, что jobs/MrrSnapshotJob.java пересчитывает MRR по расписанию; risk map отметил зону mrr-engine / pause-resume как business-critical и no-test; inventory описал flow «пауза подписки и последующее возобновление» с input/output и evidence. И только теперь вы пишете Characterization candidate: yes. Не раньше.
Это очень отрезвляет и в работе с Claude Code. Если попросить модель сразу «найти, что покрыть тестами», она почти наверняка срежет углы и выдаст советы, плохо привязанные к evidence. Если же дать ей RISK_MAP.md и BEHAVIOR_INVENTORY.md, вы вынуждаете её работать как аналитика, а не как вдохновенного угадывателя.
Если вам помогает read-only агент из Workflow Kit, хорошо, когда задачу вы ставите не героически, а скучно и по делу: собрать факты, не предлагать правок, вернуть решение и evidence. Чем скучнее и проверяемее помощник, тем меньше потом сюрпризов.
6. CashFlow Dashboard: три явных кандидата
На CashFlow Dashboard есть несколько сценариев, которые прямо напрашиваются в BEHAVIOR_INVENTORY.md и почти автоматически получают yes в колонке characterization candidate.
| Flow | Risk area | Candidate | Почему |
|---|---|---|---|
| Partial refund в середине периода | refunds / MRR | yes | Денежная логика, proration, неочевидный расчёт |
| Failed payment, затем retry success | payments / MRR | yes | Влияет на churn и continuity revenue |
| Plan switch в день биллинга | billing / MRR | yes | Пограничная дата, высокий шанс неочевидного поведения |
| Текстовое описание тарифа в help-секции | docs / UI | no | Не формирует критичный baseline для legacy-перестройки |
Посмотрим на один сценарий чуть ближе. Partial refund mid-period — классический кандидат. Опасность не только в деньгах: формула часто размазана по нескольким файлам — RefundService, биллинговый расчёт, MRR snapshot. Не зафиксируете поведение до изменений — потом не ответите: это новый баг или вы «починили» старую неочевидную ветку.
Сценарий retry success после failed payment кажется проще, но это обманка. По-человечески хочется сказать: «ну платёж же потом прошёл». А на системном уровне вопрос уже другой: был ли между failed payment и retry провал в churn-метрике, не считаем ли мы подписку непрерывной там, где была яма. Мало тестов, серьёзное влияние на метрики — кандидат почти обязателен.
А вот косметическая история с подсказкой в help-разделе сама по себе characterization candidate не заслуживает. Это не значит, что ею можно пренебречь: это значит, что baseline перед сложными изменениями строят не с неё. И это важный навык — не путать важное с просто заметным.
7. Частые поломки инвентаря на этапе discovery
На этом шаге очень легко сделать документ, который выглядит солидно, а на практике бесполезен: BEHAVIOR_INVENTORY.md превращается в список хотелок или в перечисление мелочей. Нужен не «что хотелось бы», а «что система реально делает сейчас и что из этого надо защитить».
Первая частая поломка — слишком мелкая гранулярность. Если заведёте запись на каждый условный if, получите файл, который никто не дочитает. Flow — на уровне узнаваемого бизнес-сценария. Пауза подписки — да. «Ветка else в строке 67» — нет.
Вторая поломка — отсутствие evidence в надежде «потом допишу». Не допишете, а legacy такие пустоты любит особенно нежно. Нет ссылки на код, тест, лог или вывод команды — это не запись inventory, а заметка на память. Помечайте сразу как гипотезу.
Третья неприятная вещь — смешивать текущий baseline с планом ремонта. Как только рядом появляется мысль «здесь бы переписать на отдельный сервис», inventory течёт в сторону modernization roadmap. А это уже другой артефакт и другой разговор. Здесь вы не проектируете лечение — вы делаете снимок пациента перед операцией.
И последнее. Не спешите писать сами characterization tests, даже если руки чешутся и Claude уже подсовывает заготовки. Сегодня задача — чтобы напротив важных flows стояло честное yes, а рядом лежало достаточно доказательств. С таким BEHAVIOR_INVENTORY.md legacy перестаёт быть тёмной комнатой: он всё ещё старый, местами вредный и пахнет 2019 годом — но уже понятно, за какие провода нельзя дёргать без перчаток.
Когда такой inventory собран, следующий шаг становится гораздо яснее: записи с characterization candidate = yes идут в before-change baseline и только потом превращаются в тесты. Сейчас достаточно честно отметить эти точки и не перепрыгивать к реализации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ