function paragraph(text) { return '
' + text + '
'; } function heading(text) { return '' + String(code).trim() + '';
}
function figure(src, alt, caption) {
return 'git blame показывает revision и автора, последними изменивших строки; git log помогает увидеть связанные изменения. Но это не выбор текущего ответственного. Человек мог внести механический перенос, а решение о контракте могло принадлежать другой области. Исторический автор — факт расследования, не автоматическое назначение для исправления.'),
dataTable(
'Четыре роли в одном дефекте',
['Роль', 'На какой вопрос отвечает', 'Артефакт', 'Когда работа закончена'],
[
['Владелец решения', 'Что означает статус и какое поведение допустимо?', 'Короткая запись в issue или в описании change', 'Есть явное решение и принятая граница'],
['Владелец пути кода', 'Где изменить поведение и кто поддержит этот участок?', 'Путь, тест, CODEOWNERS-правило или карточка модуля', 'Патч попал в известную область без скрытого дублирования'],
['Владелец review', 'Кто проверит контракт и побочный эффект перед merge?', 'Запрошенный reviewer и итог review', 'Есть ответ на конкретный риск, а не только одобрение файла'],
['Владелец последующего действия', 'Кто проверит выпуск, лог или открытую техническую задачу?', 'Ссылка на проверку и назначенный исполнитель', 'Результат проверки записан либо создана отдельная задача'],
],
),
paragraph('Таблица намеренно не создаёт новую иерархию должностей. У маленького модуля одна пара рук может закрыть все четыре столбца. У стыка каталогов роли расходятся. Тогда в задаче видно, что решение должен подтвердить эксперт по контракту, а путь кода — команда, которая не даст патчу сломать соседнюю форму или сборку.'),
figure(
'/assets/editorial/2019/code-ownership-three-surfaces-2019.svg',
'Схема разделяет владельца решения, пути кода, ревью и последующей проверки вокруг одного дефекта.',
'Одна ошибка проходит через четыре независимые поверхности ответственности; история Git остаётся источником фактов, а не заменой им.',
),
heading('CODEOWNERS — маршрут запроса, а не доказательство знания'),
paragraph('В Git нет стандарта CODEOWNERS: это возможность хостинга. В GitHub файл с таким именем задаёт пользователей или команды для путей, и при pull request с изменением этих путей платформа может автоматически запросить review. Поэтому файл полезен как маршрут до нужного человека, но сам по себе не подтверждает, что reviewer прочитал контракт, а владелец решения согласовал семантику.'),
paragraph('Правила ниже показывают минимальную границу. Более общий путь расположен раньше, а специфичный gateway — позже: в документации GitHub последнее подходящее правило имеет преимущество. Отдельная строка для самого CODEOWNERS не декоративна: иначе тот, кто меняет маршрут review, может незаметно назначить себе удобный маршрут. Псевдонимы в примере вымышлены и не относятся к этому репозиторию.'),
codeBlock(codeOwnersFixture),
paragraph('Не надо переписывать дерево целиком ради одного дефекта. Сначала выбираем два-три пути, по которым решение действительно проходит: обработчик статуса, контракт или fixture, рядом стоящий экран. Потом открываем pull request и смотрим, кого реально запросил хостинг из base branch. Если запрос не появился, это сигнал проверить расположение файла, регистр пути, доступ команды и порядок правил, а не повод назначить автора последнего коммита владельцем.'),
heading('Маршрут от сбоя до понятного изменения'),
orderedList([
'Записать один воспроизводимый симптом, вход и неверный результат. Не писать «сломан checkout»: указать статус, экран или функцию, где поведение наблюдается.',
'Собрать историю узкого диапазона строк и связанных путей через git blame и git log. Отделить факт «кто менял» от гипотезы «кто решает».',
'Назвать владельца решения: он отвечает на вопрос о контракте или допустимом состоянии. Если такого ответа нет, первым результатом становится именно решение, а не патч.',
'Назвать путь кода и проверить маршрут CODEOWNERS в target branch. Зафиксировать, кого нужно запросить на review и почему именно этого человека или команду.',
'Открыть небольшой change с тестом отрицательного случая. В описании указать риск, решение, reviewer и действие после merge: проверку лога, выпуска или отдельную задачу.',
'После merge выполнить запланированную проверку и записать результат рядом с задачей. Если сигнал не готов, не называть исправление полностью закрытым: есть патч, но нет следа его эксплуатации.',
]),
heading('Review проверяет риск, а не присутствие имени'),
paragraph('GitHub различает comment, approve и request changes. Это полезно использовать по смыслу. Для патча статуса reviewer по контракту должен подтвердить трактовку неизвестного значения; reviewer пути кода — проверить, что update не ломает переходы на соседнем экране. Одно «Approve» не обязано содержать оба знания. Когда роль указана рядом с вопросом, review становится коротким и предметным.'),
paragraph('Не превращайте CODEOWNERS в список людей, которых нужно позвать на любую правку. Широкое правило * годится как запасной маршрут, но не показывает, кто может решить спорный контракт. Избыточный список вырабатывает привычку к механическому approval. Лучше небольшая карта с понятными границами и отдельным назначением специалиста, когда решение выходит за границу файла.'),
heading('Что оставить после исправления'),
paragraph('Минимальный след состоит из четырёх вещей: теста на прошлый сбой, записи о решении, пути с понятным маршрутом review и назначенной проверки после merge. Не обязательно строить каталог всей архитектуры. Достаточно, чтобы следующий разработчик нашёл ответ на два вопроса: почему статус обрабатывается именно так и кто подтвердит изменение, если контракт снова придёт с другой стороны.'),
paragraph('Проверьте и отрицательный вариант. Удалите reviewer из черновой модели: становится ли понятно, что review не закрыто? Подмените неизвестный статус в тесте: остаётся ли он видимым, а не превращается в успех? Сместите файл в соседний каталог: маршрут ownership всё ещё соответствует реальной границе? Такой короткий тест вскрывает фиктивную ответственность раньше, чем она попадёт в выпуск.'),
],
[githubCodeOwners, githubReviews, gitBlame, gitLog],
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2019-12-mechanism-code-ownership',
title: 'Под капотом: Git, CODEOWNERS и границы ответственности',
categories: ['Разработка', 'Команда', 'Git'],
cover: '/assets/editorial/2019/code-ownership-resolution-2019.svg',
excerpt: 'Git показывает историю строк, CODEOWNERS маршрутизирует запрос review, а решение и эксплуатационная проверка требуют отдельных записей. Разбираем границы каждого механизма.',
readingMinutes: 15,
},
[
paragraph('Симптом обычно маскируется под простую задержку: bug report уже есть, файл найден, автор строки виден в git blame, но исправление всё равно ждёт ответа. Причина в смешении механизмов. История говорит, как строка оказалась в дереве. CODEOWNERS может направить pull request в нужную область. Review фиксирует решение по изменению. Ни один из этих следов сам не назначает человека, который подтвердит смысл контракта после инцидента.'),
paragraph('Цена смешения видна на модульных стыках. Разработчик меняет gateway, потому что он последний касался parser. Reviewer смотрит на diff и одобряет синтаксис. После merge неизвестный ответ всё ещё трактуется неверно, поскольку ни у кого не было явной обязанности решить, что этот ответ означает. Разберём механизм без легенды о «единственном настоящем владельце»: у кода, review и последующего действия разные границы.'),
heading('История строки отвечает только на исторический вопрос'),
paragraph('git blame аннотирует строку revision и автором, которые последними её изменили. Это удобно для поиска контекста, особенно с ограничением диапазона -L. Но команда не знает, был ли коммит рефакторингом, переносом форматирования, временным обходом или решением контракта. Документация Git прямо описывает изменение строк, а не право принимать сегодняшнее решение о поведении системы.'),
paragraph('Поэтому исторический поиск лучше делать двухшаговым. Сначала берём узкий диапазон и историю конкретного пути. Затем смотрим, какие документы, тесты и соседние модули упоминались в изменениях. Если история даёт несколько имён, это нормальный результат исследования: она расширяет круг вопросов, а не выбирает виноватого по дате коммита.'),
codeBlock(historyFixture),
dataTable(
'Что можно и нельзя вывести из каждого следа',
['След', 'Надёжный вывод', 'Неверный вывод', 'Следующее действие'],
[
['git blame', 'какой revision последним изменил конкретную строку', 'этот автор владеет текущим бизнес-решением', 'прочитать diff и связанный context'],
['git log по пути', 'какие commits затрагивали файл или каталог', 'история покрывает все внешние зависимости', 'сравнить с contract и тестами'],
['CODEOWNERS', 'кого платформа запросит на review для совпавшего пути', 'эти люди приняли решение или проверили production', 'проверить base branch, порядок правил и доступ'],
['Review в PR', 'какое решение reviewer отправил для этого diff', 'кто выполнит проверку после merge', 'назначить follow-up отдельно'],
],
),
paragraph('Эта граница не обесценивает Git. Наоборот, она делает расследование короче: не спорим с историей, а используем её по назначению. Если git blame показывает технического автора, а контракт указывает на другую область, задача должна сохранить оба факта. Пропасть между ними — не ошибка инструмента, а место, где нужен явный владелец решения.'),
figure(
'/assets/editorial/2019/code-ownership-resolution-2019.svg',
'Последовательность: симптом, исторический факт Git, маршрут CODEOWNERS, решение review и отдельная проверка после merge.',
'Механизмы расположены по роли: Git помогает расследовать, CODEOWNERS направляет запрос, review принимает изменение, а эксплуатационное действие остаётся отдельным обязательством.',
),
heading('CODEOWNERS вычисляет маршрут по пути и base branch'),
paragraph('CODEOWNERS — не часть формата Git-репозитория, а правило конкретной платформы. В GitHub файл может находиться в .github/, корне или docs/; для запроса code-owner review используется вариант из base branch pull request. Это важная деталь: изменение правила в feature branch не должно позволить автору change переназначить reviewer для того же merge.'),
paragraph('Синтаксис похож на .gitignore, но не полностью совпадает. GitHub отдельно предупреждает, что отрицание через ! и диапазоны в квадратных скобках не работают как в gitignore. Для проекта это не повод писать большой исключающий список. Проще выбрать непрерывные каталоги и положить более специфичное правило ниже общего, потому что последнее совпадение имеет преимущество.'),
codeBlock(codeOwnersFixture),
paragraph('В учебной карте общий owner ловит всё дерево, checkout — экранную область, а gateway — более узкий интеграционный путь. Строка документа контракта имеет двух owners, поскольку изменение текста способно поменять решение и реализацию сразу. Последняя строка защищает сам механизм маршрутизации. В настоящем проекте вместо учебных псевдонимов нужны реальные пользователи или видимые команды с нужными правами; иначе GitHub не назначит code owner.'),
heading('Запрос review и его результат — разные состояния'),
paragraph('Автоматический запрос reviewer ещё не означает review. В GitHub итог review бывает comment, approve или request changes. Политика защищённой ветки может требовать approval, но сам факт участия code owner не заменяет список вопросов. У маленького change его стоит написать прямо: «проверьте трактовку unknown status» или «подтвердите, что retry не создаёт второй transition».'),
paragraph('Это особенно важно, если один path имеет несколько owners. Одобрение одного code owner может быть достаточно для правила платформы, но продуктовый риск иногда требует двух разных ответов. Не стоит выдавать локальную настройку merge за модель знаний команды. Если решение касается API-контракта и UI-перехода, запросите соответствующих людей и сохраните в описании, что именно каждый из них подтвердил.'),
heading('Последующее действие не хранится в diff автоматически'),
paragraph('После merge появляется другой вопрос: доказал ли выпуск, что выбранное решение работает на настоящем пути? Ни Git commit, ни CODEOWNERS, ни approve сами по себе не создают этот ответ. В 2019 году достаточно простого артефакта: назначить человека на проверку лога или сценария, указать срок и записать ожидаемый сигнал. Это не попытка сделать из одной команды круглосуточную службу; это защита от случая «код уже merged, а баг всё ещё ничей». '),
paragraph('Если проверка должна быть выполнена другой группой, не прячьте её в комментарии к PR. Откройте отдельную задачу, свяжите её с change и оставьте один исход: подтверждённый результат, откат или новая проблема. Так reviewer может закончить работу с diff, а владелец follow-up получает свою собственную проверяемую очередь.'),
heading('Собираем минимальный контракт без лишнего процесса'),
orderedList([
'Выбрать один дефект и один точный вопрос, который требует решения. Сначала записать симптом, а не искать владельца по списку сотрудников.',
'Взять историю только нужных строк и путей. Сохранить ссылки или revision hashes как контекст, не превращая их в поле «ответственный».',
'Проверить, какое правило CODEOWNERS совпадает в base branch и кого платформа запросит на review. Если путь спорный, назвать owner решения в описании change.',
'Разбить review на вопросы: семантика контракта, риск реализации, тест отрицательного случая. Один человек может закрыть несколько вопросов, но это видно явно.',
'До merge назначить последующее действие и его сигнал. Например: проверить, что unknown status остаётся отдельным состоянием, а не считается успешным.',
'После проверки закрыть след или создать новую задачу с тем же контекстом. Не оставлять результат в памяти reviewer или в неразрешённом комментарии.',
]),
heading('Типичные ложные упрощения'),
paragraph('Первое: назначить автором решения человека из git blame. Это ломается на рефакторинге и переносе кода. Второе: считать CODEOWNERS каталогом экспертов. Файл знает путь и права платформы, но не знает, кто согласовал изменение за пределами пути. Третье: считать review завершённым после появления аватара. Решение review должно отвечать на риск, а после merge ещё остаётся проверка фактического результата.'),
paragraph('Здоровая минимальность выглядит скучно: один symptom, один owner решения, один или два reviewer с конкретными вопросами, тест, запись после merge. Но именно она переживает смену людей и перенос модулей. Следующий разработчик не обязан угадывать логику по автору строки: он видит границу, маршрут проверки и место, куда возвращаться при новом варианте сбоя.'),
],
[gitBlame, gitLog, githubCodeOwners, githubReviews],
);
const fieldArticle = createRevision(
{
slug: 'editorial-2019-12-field-code-ownership',
title: 'Разбор: баг на стыке модулей и четыре владельца одного решения',
categories: ['Разработка', 'Команда', 'Git'],
cover: '/assets/editorial/2019/code-ownership-simulated-timeline-2019.svg',
excerpt: 'Анонимный учебный разбор: неизвестный ответ интеграции проходит через gateway и checkout. Отделяем исторического автора от владельцев решения, review и проверки после merge.',
readingMinutes: 15,
},
[
paragraph('Ниже — полностью вымышленный учебный сценарий. В нём нет истории частного репозитория, реального инцидента, пользователей или данных команды. Он нужен для одного практического вопроса: что делать, когда bug пересекает два модуля, а имя автора последней строки не отвечает на вопрос о поведении продукта. Симптом в симуляции такой: checkout получает неизвестный статус от gateway и показывает пользователю «успешно». Цена — неверное действие на экране и спор о том, чей это дефект.'),
paragraph('Плохой старт выглядит так: открыть git blame, найти имя и написать ему «посмотри». Хороший старт короче, но точнее: зафиксировать статус, путь, ожидаемое поведение и три разные ответственности. Кто решает семантику статуса? Кто меняет участок кода? Кто должен посмотреть pull request? Кто проверяет результат после merge? В учебной карточке все ответы записаны рядом, поэтому разбор можно повторить без личной памяти автора.'),
heading('Граница сценария и исходные факты'),
paragraph('Симулированный сервис принимает ответ unknown от внешнего gateway. На фронтенде обработчик по умолчанию сворачивает неизвестное значение в успешный переход. История конкретной строки показывает, что её последним менял разработчик из команды checkout во время переноса parser. В каталоге рядом лежит документ с договорённостью о статусах, а path gateway закреплён за другой областью. Ни один из этих фактов не называет решение сам по себе.'),
paragraph('Вместо попытки выбрать «правильного владельца» вначале формулируем критерий. Исправление готово, если неизвестный статус становится наблюдаемым отдельным состоянием, тест не даёт ему попасть в success, reviewer по контракту подтвердил трактовку, а после merge есть назначенная проверка. Это можно сделать и без доступа к production: проверка в сценарии — отдельный пункт, а не обещание о реально просмотренных данных.'),
dataTable(
'Исходные факты учебного разбора',
['Наблюдение', 'Что оно доказывает', 'Чего оно не доказывает', 'Кому задать вопрос'],
[
['git blame у parser-ветки', 'строку последним изменил участник checkout', 'он владеет правилом статусов', 'автору change — о контексте переноса'],
['Путь /web/checkout/gateway/', 'изменение попадёт в узкую интеграционную область', 'конкретный reviewer уже согласен с semantic change', 'владельцу пути и owner решения'],
['Документ контракта', 'для статусов есть место, где ожидается правило', 'документ актуален без проверки', 'владельцу решения — о норме и исключении'],
['CODEOWNERS на base branch', 'платформа может запросить review у совпавших owners', 'после merge кто-то проверит результат', 'author change — о follow-up'],
],
),
paragraph('Даже в учебном случае важно не подменять факт трактовкой. git blame корректно отвечает про последнюю модификацию линии, GitHub CODEOWNERS — про маршрут reviewer для пути. Решение о неизвестном status появляется только тогда, когда его кто-то формулирует: например, «неизвестное значение не может стать success; показываем отдельный state и сохраняем диагностический идентификатор».'),
figure(
'/assets/editorial/2019/code-ownership-simulated-timeline-2019.svg',
'Вертикальная учебная шкала: симптом неизвестного статуса, исторический поиск, назначение ролей, review, merge и отдельная проверка.',
'Все события на схеме — симуляция. Она показывает, почему автор строки, reviewer и владелец проверки могут быть разными ролями.',
),
heading('Шаг 1. Собрать карту, не выбирая виноватого'),
paragraph('В карточке дефекта создаём четыре поля. Decision owner подтверждает, что unknown означает для контракта. Code path owner помогает найти обработчик, fixture и соседние переходы. Reviewers смотрят конкретные риски в change. Follow-up owner проверяет, что после merge есть наблюдаемый результат. Английские подписи здесь только потому, что они часто встречаются в интерфейсах Git-хостингов; содержание полей остаётся простым и русским.'),
codeBlock(simulatedIssueFixture),
paragraph('Фикстура не запускает внешний gateway и не изображает production. Она фиксирует форму записи: у искусственной задачи есть отдельный follow-up owner и три условия готовности. Реальный проект может хранить это в issue, pull request template или в документе рядом с контрактом. Выбирайте место, которое команда действительно читает при изменении, а не ещё один каталог ради процесса.'),
heading('Шаг 2. Проверить маршрут review по пути'),
paragraph('Для simulated change затронуты /web/checkout/gateway/status.js и /docs/checkout-contract.md. В примере CODEOWNERS узкий путь gateway должен идти после общего checkout, иначе он не получит приоритет. Документ имеет два names: один отвечает за контракт, второй — за экран. Если хостинг не поддерживает CODEOWNERS, ту же карту можно положить в описание задачи и запросить review вручную; меняется автоматизация, а не сами роли.'),
paragraph('Перед созданием pull request автор должен посмотреть именно base branch. GitHub использует CODEOWNERS из ветки, которую change собирается изменить, и автоматически запрашивает owners для совпавших путей. Поэтому строка, добавленная только в feature branch, не доказывает, что reviewer будет назначен. В учебной проверке это не реальный PR, а вопрос к конфигурации, который нужно проверить в конкретном хостинге.'),
heading('Шаг 3. Превратить review в два проверяемых вопроса'),
paragraph('Первый review-вопрос адресован владельцу решения: допустимо ли показывать unknown как отдельное состояние, какие поля должны сохраниться для диагностики, можно ли повторить запрос. Второй — владельцу пути: не ломает ли новая ветка переход по кнопке, retry или тесты соседнего экрана. В одном pull request эти вопросы могут закрыть два человека или один. Главное — не скрыть второй вопрос за общим «looks good». '),
paragraph('В GitHub reviewer может отправить comment, approve или request changes. Для учебного change комментарий с вопросом к contract не равен approval; approval после правки не отменяет follow-up. Если требуемое review контролируется правилами ветки, платформа может не дать merge без нужного approval. Но и тогда задача должна содержать смысл: какое правило мы проверяем и какой тест доказывает прошлый сбой.'),
orderedList([
'Создать issue с исходным симптомом, входом unknown, неверным успехом и ожидаемым отдельным состоянием. Пометить сценарий как учебный, если это тренировочный материал.',
'Собрать history нужных строк и путей. Внести hashes или ссылки как факты, но не переносить имя из blame в поле decision owner без разговора о контракте.',
'Назначить owner решения, пути кода, review и follow-up. Если одна роль неизвестна, stop: это незакрытый риск, а не место для случайного назначения.',
'Проверить CODEOWNERS на base branch либо вручную запросить людей. Указать в PR два review-вопроса и приложить тест, где unknown не ведёт к success.',
'После request changes обновить change и снова запросить review, потому что смысл diff мог заметно поменяться. После approve выполнить запланированную проверку после merge.',
'Закрыть issue только с результатом follow-up: сигнал подтверждён, обнаружен новый дефект или создана связанная задача. «Merged» описывает состояние кода, но не ответ на наблюдаемый симптом.',
]),
heading('Шаг 4. После merge не теряем технический след'),
paragraph('В симуляции follow-up owner из checkout проверяет, что в выбранном контуре неизвестный статус не превращается в success и есть диагностический след. Если для проекта доступен только тестовый контур, так и пишем: «проверено на fixture», а не «исправлено везде». Если проверка требует другой команды, оставляем отдельную задачу с тем же идентификатором сценария. Это даёт следующему человеку путь от сигнала к решению без поиска по именам.'),
paragraph('Сюда же относится и документ контракта. Он не должен повторять весь код; ему хватает перечислить допустимые статусы, владельца решения и ссылку на тест. Когда новый gateway-ответ появится через полгода, изменение начнётся с проверки договорённости, а не с очередного угадывания по истории строки.'),
heading('Что в этом сценарии не стоит заявлять'),
paragraph('У разборщика нет оснований говорить, что реальная команда увидела этот инцидент, что конкретный reviewer прочитал diff или что production-метрика изменилась. Сценарий специально анонимизирован и симулирован. Его ценность не в правдоподобной легенде, а в маршруте, который можно применить к настоящей задаче: отделить факты истории от ответственности, назначить review по риску и не забыть последнюю проверку после merge.'),
paragraph('Если в реальном проекте нет CODEOWNERS или pull request workflow, не копируйте интерфейс чужой платформы. Оставьте ту же таблицу ролей в issue, добавьте путь модуля и обязуйте change иметь два ответа: кто подтвердил решение и кто подтвердил результат. Это более полезно, чем файл с владельцами, который никто не обновляет при переносе каталога.'),
],
[gitBlame, gitLog, githubCodeOwners, githubReviews],
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle];
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions));
}