revise December 2019 ownership articles
Build and deploy / deploy (push) Successful in 13s

This commit is contained in:
2026-07-31 11:10:23 +03:00
parent 8e2ea8dee8
commit f204e2d730
7 changed files with 518 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 67 из 358 созданных материалов. Остальные 291 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 70 из 358 созданных материалов. Остальные 288 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
+123
View File
@@ -0,0 +1,123 @@
# P22 — декабрь 2019: ответственность за код
Статус: **принят в publication registry 31 июля 2026 года**. Три revision
применяются по стабильным slug и сохраняют дату и автора базового архива:
- `editorial-2019-12-practice-code-ownership`;
- `editorial-2019-12-mechanism-code-ownership`;
- `editorial-2019-12-field-code-ownership`.
Модуль экспортирует ровно три revision без полей `date` и `author`. При
прямом запуске с `--print-revisions` stdout содержит только JSON, совпадающий
с import-safe export. Все псевдонимы `@example`, issue `SIM-2019-12-17` и
события полевого разбора — учебная симуляция; они не описывают частный
репозиторий, команду, пользователей или реальный инцидент.
## Проход 1. Факты и техника — пройдено
| Утверждение или решение | Официальный источник | Проверенная граница |
| --- | --- | --- |
| `git blame` показывает revision и автора, последними изменивших каждую строку | [Git: git-blame](https://git-scm.com/docs/git-blame) | Историческая атрибуция не выдана за назначение текущего владельца решения; в статьях сначала исследуется context, затем роль фиксируется отдельно |
| `git log` нужен для истории узких путей и связанных commits | [Git: git-log](https://git-scm.com/docs/git-log) | История файла не названа полной картой внешних зависимостей; в поле зрения остаются контракт и соседние modules |
| `CODEOWNERS` определяет людей или teams для путей, а review request берётся по правилам base branch pull request | [GitHub Docs: About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) | Механизм назван возможностью GitHub, а не частью стандарта Git; feature branch не объявлена источником route для собственного merge |
| В GitHub последнее совпадающее CODEOWNERS-правило имеет приоритет, а `!` и диапазоны `[]` не работают как в `.gitignore` | [GitHub Docs: About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) | Пример использует общий и более узкий path без ложного «исключения»; отдельной строкой закрыт сам `.github/CODEOWNERS` |
| Pull request review имеет решения Comment, Approve и Request changes; защита ветки может требовать approval | [GitHub Docs: Pull request reviews](https://docs.github.com/en/pull-requests/reference/pull-request-reviews) | Автоматический request reviewer не назван выполненным review, а approval не подменяет проверку результата после merge |
### Честная граница исследования
Пакет использует публичную официальную документацию Git и GitHub. В нём нет
выводов о реальной конфигурации GitHub, branch protection, составе teams или
истории какого-либо закрытого репозитория. `CODEOWNERS` в примерах —
псевдоконфигурация с `@example`; она показывает порядок и границы механизма,
но не была отправлена в хостинг для проверки actual reviewer request.
Технический принцип в трёх статьях сознательно уже: история Git — источник
факта, CODEOWNERS — маршрут запроса review, итог review — решение по diff,
follow-up — самостоятельное действие после merge. Инструменты не объединены в
фиктивного «единственного владельца».
Вердикт прохода: **пройден**. Нормативные свойства привязаны к первичным
документам, а зависящие от конкретного хостинга и команды результаты помечены
как будущая проверка интеграционного этапа.
## Проход 2. Редактура и голос M2 / 2019 — пройдено
| Ревизия | Симптом и цена в начале | Главный технический вопрос | Артефакт и ограничение |
| --- | --- | --- | --- |
| Практика | Дефект стоит на месте, потому что автор строки, владелец решения и reviewer смешаны; цена — повторный bug и задержка изменения | Как разложить ownership одного change на решение, путь кода, review и follow-up | Таблица ролей, пример CODEOWNERS, маршрут из шести шагов; платформа не заявлена настроенной |
| Механизм | `git blame` уже назвал автора, но смысл статуса всё ещё не решён; цена — ложный выбор ответственного | Где заканчивается история Git, что именно делает CODEOWNERS и что остаётся после review | Сопоставление следов, команды Git и схема потока; нет утверждения о правах или policy конкретного репозитория |
| Полевой разбор | Неизвестный gateway status превращён в success, а дефект пересекает два module; цена — неверное действие на экране | Как провести issue/review timeline без легенды о реальном инциденте | Анонимизированная fixture `SIM-2019-12-17`, таблица фактов и route; симуляция не выдана за production-проверку |
- Основной текст: практика — **7 118** знаков, механизм — **8 118**,
полевой разбор — **8 616**. Все значения в коридоре 5 000–15 000 и
рассчитаны gate без раздела источников.
- Во всех статьях есть ранние «симптом», «ошибка» или конкретный сбой с ценой;
далее выдержана цепочка: симптом → причина смешения ролей → проверка →
действие → граница знания.
- Голос соответствует M2 / 2019: автор говорит о Git, путях, тесте,
контракте, pull request и review предметно. Он не приписывает себе SLO,
организационные метрики, приватные процессы или поздний управленческий
манифест.
- У каждой revision не менее пяти смысловых разделов, доступная таблица с
`caption` и `thead`, figure с развёрнутым alt и подписью, кодовый пример,
упорядоченный маршрут и как минимум четыре официальных ссылки.
- Из текста удалены шаблонные обещания и подмена факта словом «владелец»;
`git blame`, CODEOWNERS, review и follow-up описаны разными глаголами.
Вердикт прохода: **пройден**. Тексты стали практичными без перехода к
поздней менеджерской риторике: каждый раздел ведёт к проверяемому действию.
## Проход 3. Визуал и выпуск — пройдено в пределах автономного пакета
- `code-ownership-three-surfaces-2019.svg` сначала был слишком широк для
мобильного чтения. После отдельного визуального просмотра он переделан в
узкую вертикальную схему: symptom, решение, путь кода, review, follow-up и
Git history читаются последовательно.
- `code-ownership-resolution-2019.svg` показывает не «органиграмму», а
маршрут артефактов: наблюдаемый сбой → исторический факт → route review →
решение reviewer → проверка после merge.
- `code-ownership-simulated-timeline-2019.svg` крупно помечает simulation и
не содержит намёков на настоящую команду. Шкала отделяет поиск context от
назначения ролей.
- Все три SVG содержат `title`, `desc`, `role="img"` и `aria-labelledby`.
В них нет JavaScript, `foreignObject`, внешних URL или растровых data URI.
У figure в статьях есть отдельные alt-тексты и captions.
- SVG были отрендерены локально через Sharp в исходном размере и при ширине
**375 px**. После перестройки узкой композиции нет обрезания текста или
горизонтального выхода за границы. Это проверка самих диаграмм, не
браузерный e2e-прогон опубликованной страницы.
- Основной редактор независимо просмотрел финальные три растеризованных
схемы на 375 px; вывод подтверждён: текст не обрезан, последовательность
артефактов читается. Статьи не выдают это за проверку интерактивного
поведения хостинга или реального review-flow.
- После подключения registry strict audit и production build пройдены;
ручной browser-review остаётся отдельной проверкой и не заявлен как
выполненный.
### Фактические проверки
```text
node --check web/scripts/upgrade-2019-12.mjs
cd web && npm run audit:draft -- scripts/upgrade-2019-12.mjs
xmllint --noout \
web/public/assets/editorial/2019/code-ownership-three-surfaces-2019.svg \
web/public/assets/editorial/2019/code-ownership-resolution-2019.svg \
web/public/assets/editorial/2019/code-ownership-simulated-timeline-2019.svg
```
| Проверка | Фактический результат |
| --- | --- |
| `node --check` | PASS, код 0 |
| `--print-revisions` и import-safe export | PASS внутри `audit:draft`: stdout JSON-only и ровно три revision |
| `npm run audit:draft -- scripts/upgrade-2019-12.mjs` | PASS: 7 118 / 8 118 / 8 616 знаков, структура и локальные assets найдены |
| `xmllint --noout` для трёх SVG | PASS, код 0 |
| Локальный visual review | PASS: исходный размер и 375 px, после перестройки нет clipping или horizontal overflow внутри SVG |
| Strict audit после подключения registry | PASS: 7 118 / 8 118 / 8 616 знаков; по одному figure и table, 1 / 2 / 1 code example |
| `npm run build` | PASS, code 0, 374 статические страницы |
| Scope/self-review | PASS: в revision нет `date`/`author`, а `articles.json` не перезаписан |
Выпусковой вердикт: **тройное ревью пройдено, пакет принят к публикации**.
Registry заменяет только редакционные поля по stable slug. Production build и
visual preflight не подменяют реальную проверку поведения на выбранной
платформе review: её нужно выполнить отдельно при работе с конкретным
репозиторием.
+2
View File
@@ -18,6 +18,7 @@ import { revisions as august2019Revisions } from '../scripts/upgrade-2019-08.mjs
import { revisions as september2019Revisions } from '../scripts/upgrade-2019-09.mjs'; import { revisions as september2019Revisions } from '../scripts/upgrade-2019-09.mjs';
import { revisions as october2019Revisions } from '../scripts/upgrade-2019-10.mjs'; import { revisions as october2019Revisions } from '../scripts/upgrade-2019-10.mjs';
import { revisions as november2019Revisions } from '../scripts/upgrade-2019-11.mjs'; import { revisions as november2019Revisions } from '../scripts/upgrade-2019-11.mjs';
import { revisions as december2019Revisions } from '../scripts/upgrade-2019-12.mjs';
// This layer replaces archived source entries without losing their stable slug and date. // This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [ export const editorialRevisions = [
@@ -41,4 +42,5 @@ export const editorialRevisions = [
...september2019Revisions, ...september2019Revisions,
...october2019Revisions, ...october2019Revisions,
...november2019Revisions, ...november2019Revisions,
...december2019Revisions,
]; ];
@@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 940" role="img" aria-labelledby="title desc">
<title id="title">Маршрут от дефекта к разделённой ответственности</title>
<desc id="desc">Вертикальная схема показывает последовательность: симптом, история Git, правило CODEOWNERS, предметный review и отдельная проверка после merge.</desc>
<rect width="480" height="940" fill="#f8fafc" rx="24"/>
<text x="240" y="44" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#1f2937">Путь от дефекта к решению</text>
<text x="240" y="72" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#4b5563">Каждый шаг оставляет проверяемый артефакт</text>
<g font-family="Arial, sans-serif">
<rect x="34" y="110" width="412" height="104" rx="16" fill="#fee2e2" stroke="#f87171" stroke-width="2"/>
<text x="62" y="148" font-size="20" font-weight="700" fill="#7f1d1d">1. Симптом</text>
<text x="62" y="178" font-size="16" fill="#7f1d1d">unknown status попал в success</text>
<text x="62" y="200" font-size="14" fill="#7f1d1d">Запись: вход, неверный результат, цена.</text>
<path d="M240 214 L240 248" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="34" y="254" width="412" height="104" rx="16" fill="#e0f2fe" stroke="#38bdf8" stroke-width="2"/>
<text x="62" y="292" font-size="20" font-weight="700" fill="#0c4a6e">2. История Git</text>
<text x="62" y="322" font-size="16" fill="#0c4a6e">Кто и когда менял строку?</text>
<text x="62" y="344" font-size="14" fill="#0c4a6e">Артефакт: context, а не назначение роли.</text>
<path d="M240 358 L240 392" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="34" y="398" width="412" height="104" rx="16" fill="#dcfce7" stroke="#4ade80" stroke-width="2"/>
<text x="62" y="436" font-size="20" font-weight="700" fill="#14532d">3. CODEOWNERS</text>
<text x="62" y="466" font-size="16" fill="#14532d">Кого запросить по пути change?</text>
<text x="62" y="488" font-size="14" fill="#14532d">Артефакт: проверка правила в base branch.</text>
<path d="M240 502 L240 536" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="34" y="542" width="412" height="104" rx="16" fill="#fef3c7" stroke="#fbbf24" stroke-width="2"/>
<text x="62" y="580" font-size="20" font-weight="700" fill="#78350f">4. Review</text>
<text x="62" y="610" font-size="16" fill="#78350f">Кто подтвердит contract и риск кода?</text>
<text x="62" y="632" font-size="14" fill="#78350f">Артефакт: решение reviewer по вопросу.</text>
<path d="M240 646 L240 680" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="34" y="686" width="412" height="104" rx="16" fill="#fae8ff" stroke="#e879f9" stroke-width="2"/>
<text x="62" y="724" font-size="20" font-weight="700" fill="#701a75">5. Follow-up</text>
<text x="62" y="754" font-size="16" fill="#701a75">Кто проверит результат после merge?</text>
<text x="62" y="776" font-size="14" fill="#701a75">Артефакт: проверка, откат или новая задача.</text>
</g>
<rect x="34" y="836" width="412" height="64" rx="12" fill="#e2e8f0"/>
<text x="240" y="862" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="700" fill="#334155">Git даёт context. Платформа направляет review.</text>
<text x="240" y="884" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#334155">Ответственность фиксируется в change.</text>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b"/></marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 980" role="img" aria-labelledby="title desc">
<title id="title">Учебная шкала разбора ошибки на стыке модулей</title>
<desc id="desc">Все события на вертикальной шкале симулированы: неизвестный статус, исторический поиск, назначение ролей, review, merge и проверка результата.</desc>
<rect width="480" height="980" fill="#f8fafc" rx="24"/>
<text x="240" y="42" text-anchor="middle" font-family="Arial, sans-serif" font-size="21" font-weight="700" fill="#1f2937">Учебный разбор, не реальный инцидент</text>
<text x="240" y="70" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#4b5563">SIM-2019-12-17: unknown не становится success</text>
<line x1="52" y1="126" x2="52" y2="812" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
<g font-family="Arial, sans-serif">
<circle cx="52" cy="146" r="18" fill="#ef4444"/><text x="52" y="152" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">1</text>
<rect x="92" y="108" width="348" height="78" rx="14" fill="#fee2e2" stroke="#f87171" stroke-width="2"/>
<text x="116" y="138" font-size="19" font-weight="700" fill="#7f1d1d">Симптом</text><text x="116" y="164" font-size="15" fill="#7f1d1d">unknown status показан как success</text>
<circle cx="52" cy="286" r="18" fill="#0284c7"/><text x="52" y="292" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">2</text>
<rect x="92" y="248" width="348" height="78" rx="14" fill="#e0f2fe" stroke="#38bdf8" stroke-width="2"/>
<text x="116" y="278" font-size="19" font-weight="700" fill="#0c4a6e">Контекст Git</text><text x="116" y="304" font-size="15" fill="#0c4a6e">Строка и путь исследованы; роль не выбрана</text>
<circle cx="52" cy="426" r="18" fill="#16a34a"/><text x="52" y="432" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">3</text>
<rect x="92" y="388" width="348" height="78" rx="14" fill="#dcfce7" stroke="#4ade80" stroke-width="2"/>
<text x="116" y="418" font-size="19" font-weight="700" fill="#14532d">Роли назначены</text><text x="116" y="444" font-size="15" fill="#14532d">Решение, путь кода, review, follow-up</text>
<circle cx="52" cy="566" r="18" fill="#d97706"/><text x="52" y="572" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">4</text>
<rect x="92" y="528" width="348" height="78" rx="14" fill="#fef3c7" stroke="#fbbf24" stroke-width="2"/>
<text x="116" y="558" font-size="19" font-weight="700" fill="#78350f">Review change</text><text x="116" y="584" font-size="15" fill="#78350f">Contract и соседний путь проверены</text>
<circle cx="52" cy="706" r="18" fill="#a855f7"/><text x="52" y="712" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">5</text>
<rect x="92" y="668" width="348" height="78" rx="14" fill="#fae8ff" stroke="#e879f9" stroke-width="2"/>
<text x="116" y="698" font-size="19" font-weight="700" fill="#701a75">После merge</text><text x="116" y="724" font-size="15" fill="#701a75">Есть назначенная проверка результата</text>
</g>
<rect x="30" y="850" width="420" height="76" rx="14" fill="#e2e8f0"/>
<text x="240" y="880" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="700" fill="#334155">Все шаги выше — симуляция.</text>
<text x="240" y="904" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#334155">Это не сведения о команде или репозитории.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 980" role="img" aria-labelledby="title desc">
<title id="title">Четыре поверхности ответственности за изменение</title>
<desc id="desc">Симулированный дефект связан с владельцем решения, путём кода, ревью и проверкой после merge. История Git показана как источник фактов, а не как владелец.</desc>
<rect width="480" height="980" fill="#f7f8fb" rx="24"/>
<text x="240" y="44" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#1f2937">Один дефект — четыре ответа</text>
<text x="240" y="72" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#4b5563">Код, review и проверка после merge</text>
<text x="240" y="92" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#4b5563">не обязаны иметь одного владельца</text>
<rect x="48" y="124" width="384" height="116" rx="18" fill="#1d4ed8"/>
<text x="240" y="166" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#ffffff">Симптом</text>
<text x="240" y="198" text-anchor="middle" font-family="Arial, sans-serif" font-size="17" fill="#dbeafe">unknown status стал success</text>
<text x="240" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#dbeafe">что именно нужно изменить?</text>
<path d="M240 240 L240 276" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="48" y="282" width="384" height="104" rx="16" fill="#dbeafe" stroke="#60a5fa" stroke-width="2"/>
<text x="78" y="322" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#1e3a8a">1. Владелец решения</text>
<text x="78" y="352" font-family="Arial, sans-serif" font-size="15" fill="#1e3a8a">Какой статус допустим по контракту?</text>
<text x="78" y="374" font-family="Arial, sans-serif" font-size="14" fill="#1e3a8a">Артефакт: явная формулировка правила.</text>
<path d="M240 386 L240 422" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="48" y="428" width="384" height="104" rx="16" fill="#dcfce7" stroke="#4ade80" stroke-width="2"/>
<text x="78" y="468" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#14532d">2. Путь кода</text>
<text x="78" y="498" font-family="Arial, sans-serif" font-size="15" fill="#14532d">Где менять parser, экран и тест?</text>
<text x="78" y="520" font-family="Arial, sans-serif" font-size="14" fill="#14532d">Артефакт: путь и отрицательный test case.</text>
<path d="M240 532 L240 568" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="48" y="574" width="384" height="104" rx="16" fill="#fef3c7" stroke="#fbbf24" stroke-width="2"/>
<text x="78" y="614" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#78350f">3. Review</text>
<text x="78" y="644" font-family="Arial, sans-serif" font-size="15" fill="#78350f">Кто проверит контракт и риск change?</text>
<text x="78" y="666" font-family="Arial, sans-serif" font-size="14" fill="#78350f">Артефакт: ответ на конкретный вопрос.</text>
<path d="M240 678 L240 714" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="48" y="720" width="384" height="104" rx="16" fill="#fae8ff" stroke="#e879f9" stroke-width="2"/>
<text x="78" y="760" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#701a75">4. После merge</text>
<text x="78" y="790" font-family="Arial, sans-serif" font-size="15" fill="#701a75">Кто проверит наблюдаемый результат?</text>
<text x="78" y="812" font-family="Arial, sans-serif" font-size="14" fill="#701a75">Артефакт: запись о проверке или задаче.</text>
<rect x="48" y="860" width="384" height="78" rx="14" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text x="78" y="892" font-family="Arial, sans-serif" font-size="18" font-weight="700" fill="#334155">Git history</text>
<text x="78" y="918" font-family="Arial, sans-serif" font-size="14" fill="#475569">Даёт факт о строке и context.</text>
<text x="78" y="938" font-family="Arial, sans-serif" font-size="14" fill="#475569">Не назначает роль автоматически.</text>
<path d="M240 824 L240 854" stroke="#94a3b8" stroke-width="3" stroke-dasharray="8 7" marker-end="url(#arrow-muted)"/>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b"/></marker>
<marker id="arrow-muted" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/></marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+284
View File
@@ -0,0 +1,284 @@
function paragraph(text) {
return '<p>' + text + '</p>';
}
function heading(text) {
return '<h2>' + text + '</h2>';
}
function codeBlock(code) {
return '<pre><code>' + String(code).trim() + '</code></pre>';
}
function figure(src, alt, caption) {
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
}
function orderedList(items) {
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
}
function dataTable(caption, headers, rows) {
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
return '<div class="table-scroll"><table><caption>' + caption + '</caption>' + head + body + '</table></div>';
}
function sourceList(items) {
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function createRevision(meta, bodyParts, sources) {
if (sources.length < 2) {
throw new Error(meta.slug + ': at least two official sources are required');
}
return {
...meta,
contentHtml: bodyParts.join('\n') + '\n' + heading('Проверяемые источники') + '\n' + sourceList(sources),
};
}
const githubCodeOwners = {
title: 'GitHub Docs: About code owners',
url: 'https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners',
note: 'CODEOWNERS назначает владельцев путей, запрашивает их review для изменений и использует правила из base branch pull request',
};
const githubReviews = {
title: 'GitHub Docs: Pull request reviews',
url: 'https://docs.github.com/en/pull-requests/reference/pull-request-reviews',
note: 'review имеет отдельные решения Comment, Approve и Request changes; правила ветки могут требовать approval перед merge',
};
const gitBlame = {
title: 'Git: git-blame documentation',
url: 'https://git-scm.com/docs/git-blame',
note: 'git blame показывает revision и автора, которые последними изменили каждую строку; это исторический факт, а не назначение текущего владельца решения',
};
const gitLog = {
title: 'Git: git-log documentation',
url: 'https://git-scm.com/docs/git-log',
note: 'git log позволяет ограничивать просмотр истории, чтобы собрать контекст изменения до исправления',
};
const codeOwnersFixture = [
'# .github/CODEOWNERS',
'# Псевдонимы и пути ниже учебные: это не фрагмент рабочего репозитория.',
'* @example/platform-review',
'/web/checkout/ @example/checkout',
'/web/checkout/gateway/ @example/payments',
'/docs/checkout-contract.md @example/payments @example/checkout',
'/.github/CODEOWNERS @example/repository-admins',
].join('\n');
const historyFixture = [
'# Сначала собираем факты, не назначая владельца по одной строке.',
'git blame -L 42,72 -- web/checkout/gateway/status.js',
'git log --follow -- web/checkout/gateway/status.js',
'git log -- web/checkout/gateway/ docs/checkout-contract.md',
'',
'# Затем фиксируем отдельные роли в задаче или pull request.',
'decision owner: @example/payments',
'code path owner: @example/checkout',
'reviewer: @example/payments + @example/checkout',
'follow-up owner: @example/checkout',
].join('\n');
const simulatedIssueFixture = [
'const issue = {',
" id: 'SIM-2019-12-17',",
" symptom: 'checkout показывает успешную оплату при неизвестном статусе',",
" decisionOwner: 'payments',",
" codePathOwner: 'checkout',",
" reviewers: ['payments', 'checkout'],",
" followUpOwner: 'checkout',",
" done: ['unknown status is visible', 'contract test exists', 'review is resolved'],",
'};',
'',
'function canClose(record) {',
' return record.done.length === 3 && Boolean(record.followUpOwner);',
'}',
'',
'canClose(issue); // true only for this simulated record',
].join('\n');
const practiceArticle = createRevision(
{
slug: 'editorial-2019-12-practice-code-ownership',
title: 'Код не равен решению: как назначить владельцев на стыке модулей',
categories: ['Разработка', 'Команда', 'Git'],
cover: '/assets/editorial/2019/code-ownership-three-surfaces-2019.svg',
excerpt: 'Баг проходит через несколько модулей, а у решения нет владельца. Разделяем владение кодом, ревью и последующим действием, чтобы исправление не осталось ничьим.',
readingMinutes: 14,
},
[
paragraph('Симптом неприятно знаком: в релизе находится ошибка на стыке checkout и интеграции, но в обсуждении сразу появляются три разных ответа. Один человек последний менял строку, второй держит каталог, третий принимает решение о статусах платежа. Пока их называют одним словом «владелец», исправление стоит на месте: reviewer не знает, что проверить, а после merge никто не берёт на себя наблюдение за повтором. Цена — не формальная задержка, а второй дефект с тем же решением, только в другом файле.'),
paragraph('В конце 2019 года для такой ситуации не нужен управленческий слой поверх всего проекта. Достаточно собрать короткую карту: кто отвечает за смысл решения, кто поддерживает конкретный путь в коде, кого запросить на review и кто закроет технический след после выпуска. Эти роли могут совпасть у одного человека. Важно не совпадение, а то, что в задаче их можно различить и проверить.'),
heading('Сначала называем не владельца, а поломку'),
paragraph('Фраза «у модуля нет хозяина» ничего не даёт для следующего шага. Полезнее начать с наблюдаемого сбоя: «неизвестный статус от gateway отображается как успешная оплата», «ответ после retry перезаписывает более новый», «правка контракта прошла без человека, который знает допустимые статусы». В этой формулировке уже видны граница, стоимость и вопрос, который должен получить решение.'),
paragraph('История Git помогает собрать контекст. Команда <code>git blame</code> показывает revision и автора, последними изменивших строки; <code>git log</code> помогает увидеть связанные изменения. Но это не выбор текущего ответственного. Человек мог внести механический перенос, а решение о контракте могло принадлежать другой области. Исторический автор — факт расследования, не автоматическое назначение для исправления.'),
dataTable(
'Четыре роли в одном дефекте',
['Роль', 'На какой вопрос отвечает', 'Артефакт', 'Когда работа закончена'],
[
['Владелец решения', 'Что означает статус и какое поведение допустимо?', 'Короткая запись в issue или в описании change', 'Есть явное решение и принятая граница'],
['Владелец пути кода', 'Где изменить поведение и кто поддержит этот участок?', 'Путь, тест, CODEOWNERS-правило или карточка модуля', 'Патч попал в известную область без скрытого дублирования'],
['Владелец review', 'Кто проверит контракт и побочный эффект перед merge?', 'Запрошенный reviewer и итог review', 'Есть ответ на конкретный риск, а не только одобрение файла'],
['Владелец последующего действия', 'Кто проверит выпуск, лог или открытую техническую задачу?', 'Ссылка на проверку и назначенный исполнитель', 'Результат проверки записан либо создана отдельная задача'],
],
),
paragraph('Таблица намеренно не создаёт новую иерархию должностей. У маленького модуля одна пара рук может закрыть все четыре столбца. У стыка каталогов роли расходятся. Тогда в задаче видно, что <em>решение</em> должен подтвердить эксперт по контракту, а <em>путь кода</em> — команда, которая не даст патчу сломать соседнюю форму или сборку.'),
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»: указать статус, экран или функцию, где поведение наблюдается.',
'Собрать историю узкого диапазона строк и связанных путей через <code>git blame</code> и <code>git log</code>. Отделить факт «кто менял» от гипотезы «кто решает».',
'Назвать владельца решения: он отвечает на вопрос о контракте или допустимом состоянии. Если такого ответа нет, первым результатом становится именно решение, а не патч.',
'Назвать путь кода и проверить маршрут CODEOWNERS в target branch. Зафиксировать, кого нужно запросить на review и почему именно этого человека или команду.',
'Открыть небольшой change с тестом отрицательного случая. В описании указать риск, решение, reviewer и действие после merge: проверку лога, выпуска или отдельную задачу.',
'После merge выполнить запланированную проверку и записать результат рядом с задачей. Если сигнал не готов, не называть исправление полностью закрытым: есть патч, но нет следа его эксплуатации.',
]),
heading('Review проверяет риск, а не присутствие имени'),
paragraph('GitHub различает comment, approve и request changes. Это полезно использовать по смыслу. Для патча статуса reviewer по контракту должен подтвердить трактовку неизвестного значения; reviewer пути кода — проверить, что update не ломает переходы на соседнем экране. Одно «Approve» не обязано содержать оба знания. Когда роль указана рядом с вопросом, review становится коротким и предметным.'),
paragraph('Не превращайте CODEOWNERS в список людей, которых нужно позвать на любую правку. Широкое правило <code>*</code> годится как запасной маршрут, но не показывает, кто может решить спорный контракт. Избыточный список вырабатывает привычку к механическому 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 уже есть, файл найден, автор строки виден в <code>git blame</code>, но исправление всё равно ждёт ответа. Причина в смешении механизмов. История говорит, как строка оказалась в дереве. CODEOWNERS может направить pull request в нужную область. Review фиксирует решение по изменению. Ни один из этих следов сам не назначает человека, который подтвердит смысл контракта после инцидента.'),
paragraph('Цена смешения видна на модульных стыках. Разработчик меняет gateway, потому что он последний касался parser. Reviewer смотрит на diff и одобряет синтаксис. После merge неизвестный ответ всё ещё трактуется неверно, поскольку ни у кого не было явной обязанности решить, что этот ответ означает. Разберём механизм без легенды о «единственном настоящем владельце»: у кода, review и последующего действия разные границы.'),
heading('История строки отвечает только на исторический вопрос'),
paragraph('<code>git blame</code> аннотирует строку revision и автором, которые последними её изменили. Это удобно для поиска контекста, особенно с ограничением диапазона <code>-L</code>. Но команда не знает, был ли коммит рефакторингом, переносом форматирования, временным обходом или решением контракта. Документация 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. Наоборот, она делает расследование короче: не спорим с историей, а используем её по назначению. Если <code>git blame</code> показывает технического автора, а контракт указывает на другую область, задача должна сохранить оба факта. Пропасть между ними — не ошибка инструмента, а место, где нужен явный владелец решения.'),
figure(
'/assets/editorial/2019/code-ownership-resolution-2019.svg',
'Последовательность: симптом, исторический факт Git, маршрут CODEOWNERS, решение review и отдельная проверка после merge.',
'Механизмы расположены по роли: Git помогает расследовать, CODEOWNERS направляет запрос, review принимает изменение, а эксплуатационное действие остаётся отдельным обязательством.',
),
heading('CODEOWNERS вычисляет маршрут по пути и base branch'),
paragraph('CODEOWNERS — не часть формата Git-репозитория, а правило конкретной платформы. В GitHub файл может находиться в <code>.github/</code>, корне или <code>docs/</code>; для запроса code-owner review используется вариант из base branch pull request. Это важная деталь: изменение правила в feature branch не должно позволить автору change переназначить reviewer для того же merge.'),
paragraph('Синтаксис похож на <code>.gitignore</code>, но не полностью совпадает. GitHub отдельно предупреждает, что отрицание через <code>!</code> и диапазоны в квадратных скобках не работают как в <code>gitignore</code>. Для проекта это не повод писать большой исключающий список. Проще выбрать непрерывные каталоги и положить более специфичное правило ниже общего, потому что последнее совпадение имеет преимущество.'),
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('Первое: назначить автором решения человека из <code>git blame</code>. Это ломается на рефакторинге и переносе кода. Второе: считать 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('Плохой старт выглядит так: открыть <code>git blame</code>, найти имя и написать ему «посмотри». Хороший старт короче, но точнее: зафиксировать статус, путь, ожидаемое поведение и три разные ответственности. Кто решает семантику статуса? Кто меняет участок кода? Кто должен посмотреть pull request? Кто проверяет результат после merge? В учебной карточке все ответы записаны рядом, поэтому разбор можно повторить без личной памяти автора.'),
heading('Граница сценария и исходные факты'),
paragraph('Симулированный сервис принимает ответ <code>unknown</code> от внешнего 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('Даже в учебном случае важно не подменять факт трактовкой. <code>git blame</code> корректно отвечает про последнюю модификацию линии, GitHub CODEOWNERS — про маршрут reviewer для пути. Решение о неизвестном status появляется только тогда, когда его кто-то формулирует: например, «неизвестное значение не может стать success; показываем отдельный state и сохраняем диагностический идентификатор».'),
figure(
'/assets/editorial/2019/code-ownership-simulated-timeline-2019.svg',
'Вертикальная учебная шкала: симптом неизвестного статуса, исторический поиск, назначение ролей, review, merge и отдельная проверка.',
'Все события на схеме — симуляция. Она показывает, почему автор строки, reviewer и владелец проверки могут быть разными ролями.',
),
heading('Шаг 1. Собрать карту, не выбирая виноватого'),
paragraph('В карточке дефекта создаём четыре поля. <strong>Decision owner</strong> подтверждает, что <code>unknown</code> означает для контракта. <strong>Code path owner</strong> помогает найти обработчик, fixture и соседние переходы. <strong>Reviewers</strong> смотрят конкретные риски в change. <strong>Follow-up owner</strong> проверяет, что после merge есть наблюдаемый результат. Английские подписи здесь только потому, что они часто встречаются в интерфейсах Git-хостингов; содержание полей остаётся простым и русским.'),
codeBlock(simulatedIssueFixture),
paragraph('Фикстура не запускает внешний gateway и не изображает production. Она фиксирует форму записи: у искусственной задачи есть отдельный follow-up owner и три условия готовности. Реальный проект может хранить это в issue, pull request template или в документе рядом с контрактом. Выбирайте место, которое команда действительно читает при изменении, а не ещё один каталог ради процесса.'),
heading('Шаг 2. Проверить маршрут review по пути'),
paragraph('Для simulated change затронуты <code>/web/checkout/gateway/status.js</code> и <code>/docs/checkout-contract.md</code>. В примере CODEOWNERS узкий путь gateway должен идти после общего checkout, иначе он не получит приоритет. Документ имеет два names: один отвечает за контракт, второй — за экран. Если хостинг не поддерживает CODEOWNERS, ту же карту можно положить в описание задачи и запросить review вручную; меняется автоматизация, а не сами роли.'),
paragraph('Перед созданием pull request автор должен посмотреть именно base branch. GitHub использует CODEOWNERS из ветки, которую change собирается изменить, и автоматически запрашивает owners для совпавших путей. Поэтому строка, добавленная только в feature branch, не доказывает, что reviewer будет назначен. В учебной проверке это не реальный PR, а вопрос к конфигурации, который нужно проверить в конкретном хостинге.'),
heading('Шаг 3. Превратить review в два проверяемых вопроса'),
paragraph('Первый review-вопрос адресован владельцу решения: допустимо ли показывать <code>unknown</code> как отдельное состояние, какие поля должны сохраниться для диагностики, можно ли повторить запрос. Второй — владельцу пути: не ломает ли новая ветка переход по кнопке, retry или тесты соседнего экрана. В одном pull request эти вопросы могут закрыть два человека или один. Главное — не скрыть второй вопрос за общим «looks good». '),
paragraph('В GitHub reviewer может отправить comment, approve или request changes. Для учебного change комментарий с вопросом к contract не равен approval; approval после правки не отменяет follow-up. Если требуемое review контролируется правилами ветки, платформа может не дать merge без нужного approval. Но и тогда задача должна содержать смысл: какое правило мы проверяем и какой тест доказывает прошлый сбой.'),
orderedList([
'Создать issue с исходным симптомом, входом <code>unknown</code>, неверным успехом и ожидаемым отдельным состоянием. Пометить сценарий как учебный, если это тренировочный материал.',
'Собрать history нужных строк и путей. Внести hashes или ссылки как факты, но не переносить имя из blame в поле decision owner без разговора о контракте.',
'Назначить owner решения, пути кода, review и follow-up. Если одна роль неизвестна, stop: это незакрытый риск, а не место для случайного назначения.',
'Проверить CODEOWNERS на base branch либо вручную запросить людей. Указать в PR два review-вопроса и приложить тест, где <code>unknown</code> не ведёт к 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));
}