revise September 2018 environment articles
Build and deploy / deploy (push) Successful in 22s

This commit is contained in:
2026-07-31 09:52:09 +03:00
parent 7c53ffe6bc
commit 2c7f9d37d7
9 changed files with 856 additions and 2 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 24 из 358 созданных материалов. Остальные 334 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 27 из 358 созданных материалов. Остальные 331 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
@@ -19,6 +19,6 @@
- исследовательское ревью: ссылки проверены, версии и ограничения названы; - исследовательское ревью: ссылки проверены, версии и ограничения названы;
- редакторское ревью: проблема в начале, нет шаблонного языка, голос соответствует году; - редакторское ревью: проблема в начале, нет шаблонного языка, голос соответствует году;
- визуальное ревью: рисунки открываются, таблицы работают на 375px, у рисунков есть `alt` и подписи; - визуальное ревью: рисунки открываются, таблицы работают на 375px, у рисунков есть `alt` и подписи;
- `node --check`, XML-проверка диаграмм, `npm run audit:articles -- <slugs>` и production-сборка проходят. - `node --check`, `npm run audit:draft -- scripts/upgrade-YYYY-MM.mjs`, XML-проверка диаграмм, `npm run audit:articles -- <slugs>` и production-сборка проходят.
После этого рядом с партией появляется запись в `editorial/reviews/`, а изменение публикуется отдельным коммитом. Ни один скрипт не должен перегенерировать уже отревьюированный архив целиком. После этого рядом с партией появляется запись в `editorial/reviews/`, а изменение публикуется отдельным коммитом. Ни один скрипт не должен перегенерировать уже отревьюированный архив целиком.
+79
View File
@@ -0,0 +1,79 @@
# Сентябрь 2018 — черновое тройное ревью «Воспроизводимое окружение Windows»
Пакет автономный и предназначен для отдельной интеграции:
- <code>editorial-2018-09-practice-windows-dev-env</code>
- <code>editorial-2018-09-mechanism-windows-dev-env</code>
- <code>editorial-2018-09-field-windows-dev-env</code>
В этой партии созданы только:
- <code>web/scripts/upgrade-2018-09.mjs</code>;
- три SVG в <code>web/public/assets/editorial/2018/</code>;
- этот файл ревью.
<code>web/data/articles.json</code>, реестр ревизий, аудит, стандарт качества, очередь и Git не менялись. Дата ревью: 31 июля 2026 года.
## 1. Факты и техника — пройдено
### Граница и первичные источники
| Утверждение в статье | Чем сверено | Оговорка в тексте |
| --- | --- | --- |
| В Windows у переменных среды есть Machine, User и Process scopes; дочерние процессы наследуют переменные родителя | [about_Environment_Variables](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1), [Win32 Environment Variables](https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables) | Статья не утверждает, что старое окно мгновенно получает постоянную правку; проверка идёт в новом сеансе |
| <code>PATH</code> задаёт каталоги поиска, а <code>PATHEXT</code> — исполняемые расширения | [about_Environment_Variables](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1) | Это не единственный слой выбора в PowerShell: отдельно названы alias, функции, cmdlet и внешние приложения |
| <code>Get-Command -All</code> показывает несколько одноимённых команд в порядке выбора PowerShell | [Get-Command](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-command?view=powershell-5.1) | <code>where.exe</code> не выдан за эквивалент: он помогает увидеть файлы в текущем каталоге и <code>PATH</code> |
| <code>chcp</code> показывает активную кодовую страницу; смена относится к программам, стартующим после неё | [chcp](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp) | Кодовая страница консоли не приравнена к кодировке файла или настройке вывода .NET |
| JSON-снимок и дифф строятся стандартными PowerShell-командами | [ConvertTo-Json](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1), [Compare-Object](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/compare-object?view=powershell-5.1) | Сравнение показывает различающиеся строки, а не доказывает причину без повторного запуска исходной команды |
### Технический проход
- Скрипт экспортирует три ревизии и не меняет архив при import.
- Контракт CLI проверен отдельно: <code>--print-revisions</code> выдал только валидный JSON из трёх объектов, без строки Usage и без stderr.
- В примерах нет команды, отключающей execution policy, антивирус, проверку сертификатов или иной механизм безопасности.
- Опасные данные ограничены заранее выбранными полями. В тексте отдельно сказано, что <code>PATH</code> может раскрыть имя пользователя и внутренние каталоги, поэтому JSON нужно просмотреть перед передачей.
- Поля <code>PATH</code>, кодовая страница, найденный кандидат и результат <code>--version</code> не смешаны в один «универсальный диагноз». Каждая статья называет следующий наблюдаемый шаг.
- Пути и версии в полевом разборе прямо помечены учебными. Они не приписываются реальному пользователю, команде или измерению.
- В рабочей среде нет Windows PowerShell 5.1, поэтому примеры не объявлены выполненными на другой ОС. Их синтаксис, параметры и фактические утверждения сверены с первичной документацией; перед публикацией на Windows нужен отдельный локальный прогон учебных <code>.ps1</code>.
## 2. Редактура и голос М1 — пройдено
| Ревизия | Один вопрос | Симптом и цена в начале | Освоенное умение 2018 года | Основной текст |
| --- | --- | --- | --- | --- |
| Практика | Какой минимальный снимок позволяет повторить запуск | Команда не находит инструмент или выбирает другой; риск — случайная глобальная правка и утечка переменных | PowerShell-скрипт, JSON и ручная проверка пути | 5 866 знаков без кода и источников |
| Механизм | Как PowerShell выбирает команду и где участвует кодовая страница | Версия отличается и текст нечитаем; риск — исправить не тот бинарник или повредить текст | <code>Get-Command -All</code>, <code>where.exe</code>, процессный <code>PATH</code> | 6 864 знака без кода и источников |
| Поле | Как найти различие между двумя машинами без переустановки | Одинаковая команда расходится; риск — стереть след случайными установками | Снимок, <code>Compare-Object</code>, обратимый опыт в новом сеансе | 7 343 знака без кода и источников |
- Голос удержан на М1: инженерская заметка 2018 года идёт от наблюдаемой ошибки к короткой команде, затем к ручной проверке.
- Не добавлены поздние роли, SLO, Kubernetes, контейнерные платформы, продуктовые метрики или непроверяемые истории внедрения.
- В каждом тексте есть один рисунок с осмысленным <code>alt</code> и подписью, одна доступная таблица, воспроизводимый PowerShell-фрагмент, нумерованный порядок действий, ограничения и точный заголовок <code>&lt;h2&gt;Проверяемые источники&lt;/h2&gt;</code>.
- Убраны общие вводные и обещания «универсального решения». Утверждения ограничены версией, текущим процессом, конкретной командой или учебным примером.
### Третий редакторский проход — пройдено
- Независимый draft-gate вернул замечание только к <code>editorial-2018-09-field-windows-dev-env</code>: в первых 800 знаках был сценарий, но не было явного маркера проблемы.
- Первое предложение заменено минимально: оно начинается с <code>Симптом:</code>, сохраняет две Windows-машины, расходящийся запуск и цену поспешной переустановки.
- Остальная структура, объём, источники, визуал и тон М1 не менялись. Повторный <code>npm run audit:draft -- scripts/upgrade-2018-09.mjs</code> из <code>web/</code> прошёл для всех трёх slug: 8 086 / 7 690 / 8 745 знаков body; <code>node --check scripts/upgrade-2018-09.mjs</code> также завершился успешно.
## 3. Визуал и выпуск — пройдено
- <code>windows-dev-env-snapshot-2018.svg</code> объясняет состав минимального снимка и границу с секретными данными.
- <code>windows-dev-env-resolution-2018.svg</code> разделяет поиск команды и проверку кодовой страницы; это предотвращает неверный вывод, что <code>where.exe</code> полностью описывает PowerShell.
- <code>windows-dev-env-diff-2018.svg</code> показывает порядок полевого разбора: одинаковая команда → очищенные снимки → одно отличие → обратимый опыт → повтор.
- Все схемы имеют адаптивный viewBox шириной 720px, используют контрастный текст и не содержат текста, который является единственным носителем смысла: связи также видны по группам и стрелкам.
- SVG открыты через локальный Quick Look в масштабе 1200px. При первом просмотре нижняя строка схемы полевого разбора выходила за правую границу; текст сокращён, схема отрендерена повторно без обрезания. На трёх схемах проверены стрелки, контраст, кириллица и отсутствие клиппинга.
### Выполненные команды и результат
node --check web/scripts/upgrade-2018-09.mjs
node web/scripts/upgrade-2018-09.mjs --print-revisions
node --input-type=module -e "import('./web/scripts/upgrade-2018-09.mjs').then(({revisions}) => { if (revisions.length !== 3) process.exit(1); })"
xmllint --noout web/public/assets/editorial/2018/windows-dev-env-snapshot-2018.svg
xmllint --noout web/public/assets/editorial/2018/windows-dev-env-resolution-2018.svg
xmllint --noout web/public/assets/editorial/2018/windows-dev-env-diff-2018.svg
- Все шесть команд завершились успешно. Отдельно проверены чистый import, точный порядок трёх slug, 5 000–15 000 знаков основного текста, 8 / 8 / 9 смысловых заголовков, по два блока кода, figure с непустым alt и подписью, таблица с thead, нумерованный маршрут, точный раздел источников и 4 / 5 / 5 внешних первичных ссылок.
- <code>git diff --check</code> по пяти файлам партии прошёл без пробелов и конфликтных маркеров. В статусе партии — ровно эти пять новых файлов; скрипт не импортирует и не записывает архив, реестр, аудит, стандарт, очередь или Git-историю.
- После интеграции основной редактор повторно прогнал strict audit через реестр ревизий и production build. Сборка прошла и сгенерировала 374 статические страницы.
Статус: принят в публикационный слой 31 июля 2026 года. Ревизии накладываются только по стабильным slug, без изменения даты, автора и базового архива.
+2
View File
@@ -3,6 +3,7 @@ import { revisions as may2018Revisions } from '../scripts/upgrade-2018-05.mjs';
import { revisions as june2018Revisions } from '../scripts/upgrade-2018-06.mjs'; import { revisions as june2018Revisions } from '../scripts/upgrade-2018-06.mjs';
import { revisions as july2018Revisions } from '../scripts/upgrade-2018-07.mjs'; import { revisions as july2018Revisions } from '../scripts/upgrade-2018-07.mjs';
import { revisions as august2018Revisions } from '../scripts/upgrade-2018-08.mjs'; import { revisions as august2018Revisions } from '../scripts/upgrade-2018-08.mjs';
import { revisions as september2018Revisions } from '../scripts/upgrade-2018-09.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 = [
@@ -11,4 +12,5 @@ export const editorialRevisions = [
...june2018Revisions, ...june2018Revisions,
...july2018Revisions, ...july2018Revisions,
...august2018Revisions, ...august2018Revisions,
...september2018Revisions,
]; ];
+1
View File
@@ -7,6 +7,7 @@
"admin:posts": "node ../local-admin/posts-admin.mjs", "admin:posts": "node ../local-admin/posts-admin.mjs",
"build": "next build", "build": "next build",
"audit:articles": "node scripts/audit-quality-batch.mjs", "audit:articles": "node scripts/audit-quality-batch.mjs",
"audit:draft": "node scripts/audit-editorial-draft.mjs",
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 670" role="img" aria-labelledby="title desc">
<title id="title">Сравнение двух Windows-окружений</title>
<desc id="desc">Два одинаковых запуска проекта дают два очищенных снимка окружения. Сравнение выделяет различие в пути, первом кандидате бинарника или кодовой странице. Затем выполняется один обратимый опыт в новом PowerShell и повторяется исходная команда.</desc>
<defs>
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1b1622"/>
<stop offset="1" stop-color="#09131c"/>
</linearGradient>
<linearGradient id="decision" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#f0ad5e"/>
<stop offset="1" stop-color="#f87965"/>
</linearGradient>
<marker id="arrow" markerWidth="11" markerHeight="11" refX="8" refY="5.5" orient="auto">
<path d="M0,0 L9,5.5 L0,11 Z" fill="#f3b56a"/>
</marker>
<style>
.card { fill: #2a2338; stroke: #7e628f; stroke-width: 2; }
.check { fill: #132c34; stroke: #3c8d87; stroke-width: 2; }
.label { fill: #fff8f2; font-family: Arial, sans-serif; font-size: 22px; font-weight: 700; }
.copy { fill: #e6d8d1; font-family: Arial, sans-serif; font-size: 16px; }
.small { fill: #d4beb4; font-family: Arial, sans-serif; font-size: 14px; }
.code { fill: #ffcb89; font-family: "Courier New", monospace; font-size: 15px; }
.line { fill: none; stroke: #f3b56a; stroke-width: 3; marker-end: url(#arrow); }
</style>
</defs>
<rect width="720" height="670" rx="28" fill="url(#background)"/>
<text x="46" y="56" class="label">«Работает на моей машине» → сравнимые данные</text>
<text x="46" y="84" class="copy">Одна исходная команда, один снимок на машину, одна проверяемая гипотеза.</text>
<rect x="48" y="124" width="270" height="122" rx="16" class="card"/>
<text x="72" y="162" class="label">Машина A</text>
<text x="72" y="192" class="code">commit + command</text>
<text x="72" y="218" class="small">snapshot A, пути очищены</text>
<rect x="402" y="124" width="270" height="122" rx="16" class="card"/>
<text x="426" y="162" class="label">Машина B</text>
<text x="426" y="192" class="code">commit + command</text>
<text x="426" y="218" class="small">snapshot B, пути очищены</text>
<path d="M183 246 V290 H300 V304" class="line"/>
<path d="M537 246 V290 H420 V304" class="line"/>
<rect x="210" y="320" width="300" height="102" rx="18" fill="url(#decision)"/>
<text x="244" y="360" fill="#25150e" font-family="Arial, sans-serif" font-size="24" font-weight="700">Compare-Object</text>
<text x="244" y="390" fill="#382114" font-family="Arial, sans-serif" font-size="15">PATH · кандидат · версия · консоль</text>
<path d="M360 422 V456" class="line"/>
<rect x="48" y="474" width="190" height="126" rx="16" class="check"/>
<text x="70" y="512" class="label">Различие</text>
<text x="70" y="542" class="code">старый node.exe</text>
<text x="70" y="570" class="small">или кодовая страница</text>
<rect x="264" y="474" width="190" height="126" rx="16" class="check"/>
<text x="286" y="512" class="label">Опыт</text>
<text x="286" y="542" class="code">$env:Path</text>
<text x="286" y="570" class="small">только новый сеанс</text>
<rect x="480" y="474" width="192" height="126" rx="16" class="check"/>
<text x="502" y="512" class="label">Повтор</text>
<text x="502" y="542" class="code">npm run build</text>
<text x="502" y="570" class="small">исходная команда</text>
<path d="M238 537 H252" class="line"/>
<path d="M454 537 H468" class="line"/>
<text x="48" y="640" class="small">Нет изменения — проверить лог, файл или право доступа.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 680" role="img" aria-labelledby="title desc">
<title id="title">Как Windows PowerShell выбирает команду</title>
<desc id="desc">PowerShell получает блок окружения процесса с PATH и PATHEXT. Get-Command с параметром All показывает alias, функции и приложения в порядке выбора. Where.exe отдельно показывает файлы. Кодовая страница проверяется отдельной веткой.</desc>
<defs>
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#151126"/>
<stop offset="1" stop-color="#07141e"/>
</linearGradient>
<marker id="arrow" markerWidth="11" markerHeight="11" refX="8" refY="5.5" orient="auto">
<path d="M0,0 L9,5.5 L0,11 Z" fill="#8c9eff"/>
</marker>
<style>
.card { fill: #211f3b; stroke: #5a65aa; stroke-width: 2; }
.file { fill: #132b36; stroke: #317c88; stroke-width: 2; }
.label { fill: #f6f5ff; font-family: Arial, sans-serif; font-size: 22px; font-weight: 700; }
.copy { fill: #d2d7f2; font-family: Arial, sans-serif; font-size: 16px; }
.small { fill: #b5c2e1; font-family: Arial, sans-serif; font-size: 14px; }
.code { fill: #aebdff; font-family: "Courier New", monospace; font-size: 15px; }
.teal { fill: #8de2d6; font-family: "Courier New", monospace; font-size: 15px; }
.line { fill: none; stroke: #8c9eff; stroke-width: 3; marker-end: url(#arrow); }
.dash { fill: none; stroke: #58bcb3; stroke-width: 2.5; stroke-dasharray: 8 7; marker-end: url(#arrow); }
</style>
</defs>
<rect width="720" height="680" rx="28" fill="url(#background)"/>
<text x="46" y="56" class="label">Разрешение имени команды</text>
<text x="46" y="84" class="copy">Поиск бинарника и отображение текста — два разных пути.</text>
<rect x="232" y="118" width="256" height="82" rx="16" class="card"/>
<text x="258" y="153" class="label">PowerShell-процесс</text>
<text x="258" y="180" class="code">node --version</text>
<rect x="58" y="266" width="232" height="136" rx="16" class="card"/>
<text x="80" y="304" class="label">Блок среды</text>
<text x="80" y="334" class="code">PATH</text>
<text x="80" y="360" class="code">PATHEXT</text>
<text x="80" y="386" class="small">унаследован от родителя</text>
<rect x="430" y="266" width="232" height="136" rx="16" class="card"/>
<text x="452" y="304" class="label">Кодовая страница</text>
<text x="452" y="334" class="code">chcp</text>
<text x="452" y="360" class="code">OutputEncoding</text>
<text x="452" y="386" class="small">читаемость вывода</text>
<path d="M319 200 V236 H174 V254" class="line"/>
<path d="M400 200 V236 H546 V254" class="dash"/>
<rect x="48" y="470" width="192" height="138" rx="16" class="file"/>
<text x="70" y="508" class="label">Get-Command</text>
<text x="70" y="536" class="teal">alias / function</text>
<text x="70" y="560" class="teal">cmdlet / app</text>
<text x="70" y="588" class="small">порядок PowerShell</text>
<rect x="264" y="470" width="192" height="138" rx="16" class="file"/>
<text x="286" y="508" class="label">where.exe</text>
<text x="286" y="536" class="teal">node.exe</text>
<text x="286" y="560" class="teal">node.cmd</text>
<text x="286" y="588" class="small">файлы в PATH</text>
<rect x="480" y="470" width="192" height="138" rx="16" class="file"/>
<text x="502" y="508" class="label">Проверка</text>
<text x="502" y="536" class="teal">путь + версия</text>
<text x="502" y="560" class="teal">текст вывода</text>
<text x="502" y="588" class="small">одна гипотеза</text>
<path d="M174 402 V446 H144 V458" class="line"/>
<path d="M174 402 V446 H360 V458" class="line"/>
<path d="M546 402 V446 H576 V458" class="dash"/>
<path d="M240 540 H252" class="line"/>
<path d="M456 540 H468" class="line"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 640" role="img" aria-labelledby="title desc">
<title id="title">Минимальный снимок окружения Windows</title>
<desc id="desc">Проект запускает PowerShell. Из процесса извлекаются путь PATH, расширения PATHEXT, кандидаты команд, версии и кодовая страница. Проверенные поля сохраняются в JSON, а секреты исключаются.</desc>
<defs>
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#101b2d"/>
<stop offset="1" stop-color="#07101d"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#45d0c2"/>
<stop offset="1" stop-color="#6aa4ff"/>
</linearGradient>
<marker id="arrow" markerWidth="11" markerHeight="11" refX="8" refY="5.5" orient="auto">
<path d="M0,0 L9,5.5 L0,11 Z" fill="#79aaff"/>
</marker>
<style>
.card { fill: #15263d; stroke: #365b82; stroke-width: 2; }
.soft { fill: #102035; stroke: #284d72; stroke-width: 1.5; }
.label { fill: #f4f8ff; font-family: Arial, sans-serif; font-size: 22px; font-weight: 700; }
.copy { fill: #c5d7eb; font-family: Arial, sans-serif; font-size: 16px; }
.small { fill: #a8c0dc; font-family: Arial, sans-serif; font-size: 14px; }
.code { fill: #81e4d8; font-family: "Courier New", monospace; font-size: 15px; }
.line { fill: none; stroke: #79aaff; stroke-width: 3; marker-end: url(#arrow); }
</style>
</defs>
<rect width="720" height="640" rx="28" fill="url(#background)"/>
<text x="48" y="58" class="label">Минимальный снимок запуска</text>
<text x="48" y="86" class="copy">Не копия компьютера, а проверяемый путь команды.</text>
<rect x="48" y="128" width="230" height="108" rx="16" class="card"/>
<text x="72" y="168" class="label">Проект</text>
<text x="72" y="198" class="code">npm run build</text>
<text x="72" y="220" class="small">одна исходная команда</text>
<rect x="442" y="128" width="230" height="108" rx="16" class="card"/>
<text x="466" y="168" class="label">PowerShell</text>
<text x="466" y="198" class="code">process scope</text>
<text x="466" y="220" class="small">текущий сеанс</text>
<path d="M278 182 H430" class="line"/>
<rect x="48" y="302" width="182" height="142" rx="16" class="soft"/>
<text x="70" y="340" class="label">Путь</text>
<text x="70" y="370" class="code">PATH</text>
<text x="70" y="398" class="code">PATHEXT</text>
<text x="70" y="424" class="small">порядок папок</text>
<rect x="269" y="302" width="182" height="142" rx="16" class="soft"/>
<text x="291" y="340" class="label">Команды</text>
<text x="291" y="370" class="code">Get-Command</text>
<text x="291" y="398" class="code">--version</text>
<text x="291" y="424" class="small">кандидаты и вывод</text>
<rect x="490" y="302" width="182" height="142" rx="16" class="soft"/>
<text x="512" y="340" class="label">Консоль</text>
<text x="512" y="370" class="code">chcp</text>
<text x="512" y="398" class="code">OutputEncoding</text>
<text x="512" y="424" class="small">только наблюдение</text>
<path d="M557 236 V280 H139 V290" class="line"/>
<path d="M557 236 V290 H360" class="line"/>
<path d="M557 236 V290 H581" class="line"/>
<rect x="144" y="510" width="432" height="84" rx="18" fill="url(#accent)"/>
<text x="175" y="548" fill="#06101c" font-family="Arial, sans-serif" font-size="25" font-weight="700">environment-snapshot.json</text>
<text x="175" y="574" fill="#0a2030" font-family="Arial, sans-serif" font-size="15">версии, пути, кандидаты; секреты исключены</text>
<path d="M139 444 V482 H352 V498" class="line"/>
<path d="M360 444 V498" class="line"/>
<path d="M581 444 V482 H368 V498" class="line"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+141
View File
@@ -0,0 +1,141 @@
import { execFile } from 'node:child_process';
import { access, readFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const execFileAsync = promisify(execFile);
const webRoot = join(fileURLToPath(new URL('..', import.meta.url)));
const scriptArgument = process.argv[2];
if (!scriptArgument) {
throw new Error('Usage: node scripts/audit-editorial-draft.mjs <revision-script.mjs>');
}
const scriptPath = isAbsolute(scriptArgument)
? scriptArgument
: resolve(process.cwd(), scriptArgument);
const archive = JSON.parse(await readFile(join(webRoot, 'data', 'articles.json'), 'utf8'));
const archiveBySlug = new Map(archive.map((article) => [article.slug, article]));
const genericPhrases = [
'У этой модели нет магической силы',
'Материалы для проверки',
'Если держать этот порядок, решение остаётся понятным',
'В современном мире',
'очень важно',
'следует отметить',
'просто нужно',
'нужно понимать, что',
];
function count(content, expression) {
return (content.match(expression) || []).length;
}
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:quot|amp|lt|gt|#039);/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(
content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''),
);
}
const cli = await execFileAsync(process.execPath, [scriptPath, '--print-revisions'], {
cwd: dirname(scriptPath),
encoding: 'utf8',
});
if (cli.stderr.trim()) {
throw new Error('CLI wrote to stderr: ' + cli.stderr.trim());
}
let cliRevisions;
try {
cliRevisions = JSON.parse(cli.stdout);
} catch (error) {
throw new Error('CLI did not print JSON: ' + error.message);
}
const imported = await import(pathToFileURL(scriptPath).href);
const revisions = imported.revisions;
if (!Array.isArray(revisions) || revisions.length !== 3) {
throw new Error('Module must export exactly three revisions');
}
if (JSON.stringify(cliRevisions) !== JSON.stringify(revisions)) {
throw new Error('CLI revisions do not match the import-safe module export');
}
const seenSlugs = new Set();
let failed = false;
for (const revision of revisions) {
const issues = [];
const content = revision.contentHtml || '';
const body = bodyText(content);
const figures = [...content.matchAll(/<figure>([\s\S]*?)<\/figure>/g)].map((match) => match[1]);
const imageSources = [...content.matchAll(/<img[^>]+src="([^"]+)"/g)].map((match) => match[1]);
const proseWithoutCode = content
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '')
.replace(/<code>[\s\S]*?<\/code>/g, '');
if (!revision.slug || !archiveBySlug.has(revision.slug)) issues.push('slug отсутствует в базовом архиве');
if (seenSlugs.has(revision.slug)) issues.push('slug повторяется внутри пакета');
seenSlugs.add(revision.slug);
if (Object.hasOwn(revision, 'date') || Object.hasOwn(revision, 'author')) {
issues.push('ревизия не должна менять дату или автора');
}
if (body.length < 5000 || body.length > 15000) issues.push('основной текст вне 5 000–15 000 знаков');
if (revision.readingMinutes < 8) issues.push('меньше 8 минут чтения');
if (count(content, /<h2>/g) < 5) issues.push('меньше пяти смысловых разделов');
if (count(content, /<figure>/g) < 1 || imageSources.length < 1) issues.push('нет визуального объяснения');
if (count(content, /<figcaption>/g) < 1) issues.push('у рисунка нет подписи');
if (count(content, /<table>/g) < 1 || count(content, /<thead>/g) < 1) issues.push('нет доступной таблицы');
if (count(content, /<pre><code>/g) < 1) issues.push('нет воспроизводимого примера');
if (count(content, /<ol>/g) < 1) issues.push('нет последовательности действий');
if (count(content, /<a href="https?:\/\//g) < 2) issues.push('меньше двух внешних источников');
if (!content.includes('<h2>Проверяемые источники</h2>')) issues.push('нет точного раздела источников');
if (!/(проблем|ошиб|симптом|сбой|задач)/i.test(body.slice(0, 800))) {
issues.push('проблема не названа в начале текста');
}
if (proseWithoutCode.includes('undefined') || proseWithoutCode.includes('[object Object]')) {
issues.push('в тексте есть след генерации');
}
for (const phrase of genericPhrases) {
if (content.includes(phrase)) issues.push('шаблонный оборот: «' + phrase + '»');
}
for (const figure of figures) {
const image = figure.match(/<img\b[^>]*>/);
const alt = image?.[0].match(/\balt="([^"]*)"/);
if (!image || !alt || alt[1].trim().length < 12) {
issues.push('у рисунка нет содержательного alt-текста');
break;
}
}
for (const source of imageSources.filter((value) => value.startsWith('/'))) {
try {
await access(join(webRoot, 'public', source));
} catch {
issues.push('не найден visual asset: ' + source);
}
}
if (issues.length > 0) {
failed = true;
console.error('FAIL ' + revision.slug + ': ' + issues.join('; '));
} else {
console.log('PASS ' + revision.slug + ': ' + body.length + ' body chars');
}
}
if (failed) process.exitCode = 1;
+425
View File
@@ -0,0 +1,425 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const escapeHtml = (value) => String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
const paragraph = (content) => '<p>' + content + '</p>';
const heading = (content) => '<h2>' + content + '</h2>';
const codeBlock = (source) => '<pre><code>' + escapeHtml(source.trim()) + '</code></pre>';
const figure = (src, alt, caption) => [
'<figure>',
'<img src="' + src + '" alt="' + alt + '" />',
'<figcaption>' + caption + '</figcaption>',
'</figure>',
].join('');
function dataTable(headers, rows) {
const head = headers.map((header) => '<th scope="col">' + header + '</th>').join('');
const body = rows.map((row) => (
'<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>'
)).join('');
return '<div class="table-scroll"><table><thead><tr>' + head
+ '</tr></thead><tbody>' + body + '</tbody></table></div>';
}
function orderedList(items) {
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
}
function sourceList(items) {
return heading('Проверяемые источники') + '<ul>' + items.map(({ label, url }) => (
'<li><a href="' + url + '" target="_blank" rel="noopener noreferrer">' + label + '</a></li>'
)).join('') + '</ul>';
}
const sources = {
environment: {
label: 'Microsoft Learn — about_Environment_Variables для Windows PowerShell',
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1',
},
processEnvironment: {
label: 'Microsoft Learn — Environment Variables (Win32)',
url: 'https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables',
},
getCommand: {
label: 'Microsoft Learn — Get-Command и параметр -All',
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-command?view=powershell-5.1',
},
where: {
label: 'Microsoft Learn — команда where',
url: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/where',
},
chcp: {
label: 'Microsoft Learn — команда chcp',
url: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp',
},
convertToJson: {
label: 'Microsoft Learn — ConvertTo-Json',
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1',
},
compareObject: {
label: 'Microsoft Learn — Compare-Object',
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/compare-object?view=powershell-5.1',
},
};
const practiceArticle = {
slug: 'editorial-2018-09-practice-windows-dev-env',
title: 'Windows. Как сохранить минимальный снимок окружения проекта',
categories: ['Windows', 'Инструменты'],
cover: '/assets/editorial/2018/windows-dev-env-snapshot-2018.svg',
excerpt: 'Фиксируем то, что реально запускает проект на Windows: порядок PATH, найденные бинарники, версии и кодировку консоли — без копирования всего компьютера и без секретов.',
readingMinutes: 11,
contentHtml: [
paragraph('На одном Windows-компьютере проект собирается, а на другом команда <code>npm run build</code> не находит <code>node</code> или запускает другую версию PHP. Цена такой ошибки — не только потерянный вечер: в спешке легко добавить в <code>PATH</code> случайную папку, переслать коллеге пароль из переменной среды или поставить непонятный архив с интерпретатором.'),
paragraph('Здесь один вопрос: <strong>какой минимальный снимок окружения нужен, чтобы другой разработчик увидел тот же запуск проекта?</strong> Не будем архивировать весь диск и делать вид, что любая разница машины важна. Зафиксируем только то, что влияет на поиск команд, их версию и текст, который видит консоль.'),
heading('Граница задачи: сохраняю запуск, а не весь компьютер'),
paragraph('Окружение процесса — это набор строк, с которым стартует конкретная консоль и её дочерние программы. В Windows есть пользовательский, системный и процессный уровни переменных. Уже открытый PowerShell не обязан получить изменения, сделанные в окне настроек: новая консоль наследует новое значение, старая продолжает работать со своим набором. Поэтому запись «у меня установлен Node» ничего не объясняет, пока не известно, какой <code>node.exe</code> нашёл именно этот процесс.'),
paragraph('Минимальный снимок отвечает на четыре проверяемых вопроса: какая версия Windows PowerShell запустила команду; какие папки стоят в <code>PATH</code> и какие расширения допускает <code>PATHEXT</code>; какие кандидаты вернул <code>Get-Command -All</code>; что напечатала сама программа для <code>--version</code>. В отдельной строке оставляю активную кодовую страницу консоли. Этого достаточно для первого сравнения, но в снимок не попадают токены, cookie, пароли, содержимое домашней папки и полный дамп всех переменных.'),
figure(
'/assets/editorial/2018/windows-dev-env-snapshot-2018.svg',
'Схема минимального снимка Windows: проект запускает PowerShell, он передаёт дочерним процессам PATH и PATHEXT, затем фиксируются найденный бинарник, версия и кодовая страница в JSON без секретных переменных.',
'Снимок описывает путь запуска команды. Он не является резервной копией компьютера и перед отправкой всё равно требует просмотра.',
),
heading('Что кладу в файл, а что оставляю на машине'),
dataTable(
['Поле снимка', 'Как получить', 'Зачем оно нужно', 'Чего в нём не должно быть'],
[
['Версия PowerShell', '<code>$PSVersionTable.PSVersion</code>', 'От неё зависят доступные команды и формат части вывода', 'Имя пользователя и содержимое профиля'],
['Порядок <code>PATH</code>', '<code>$env:Path -split ";"</code>', 'Показывает, какая папка может дать первый бинарник', 'Полный список случайных переменных среды'],
['<code>PATHEXT</code>', '<code>$env:PATHEXT</code>', 'Объясняет, какие расширения Windows считает исполняемыми', 'Изменение системного значения ради эксперимента'],
['Кандидаты команды', '<code>Get-Command node -All</code>', 'Отделяет alias или функцию от настоящего <code>node.exe</code>', 'Непроверенный вывод из чужого скриншота'],
['Версия инструмента', '<code>node --version</code>', 'Связывает найденный путь с фактическим запуском', 'Секреты, переданные приложению аргументами'],
['Кодовая страница', '<code>cmd /c chcp</code>', 'Помогает объяснить нечитаемый вывод старой консоли', 'Глобальное переключение кодировки без проверки'],
],
),
heading('Один скрипт для Windows PowerShell 5.1'),
paragraph('Ниже не установщик и не «починка» окружения. Он только собирает отчёт рядом с проектом. Список <code>$toolChecks</code> нужно оставить коротким: добавьте туда реальные инструменты проекта, например <code>php</code> и <code>composer</code>, а не все программы из меню Пуск. Команда <code>Get-Command -All</code> намеренно сохраняет все найденные варианты, потому что первый путь без остальных кандидатов часто скрывает причину расхождения.'),
codeBlock([
'# tools/Capture-Environment.ps1',
'param(',
" [string]$OutputPath = (Join-Path $PSScriptRoot 'environment-snapshot.json')",
')',
'',
"$ErrorActionPreference = 'Stop'",
'$toolChecks = @(',
" [pscustomobject]@{ name = 'node'; arguments = @('--version') },",
" [pscustomobject]@{ name = 'npm'; arguments = @('--version') },",
" [pscustomobject]@{ name = 'php'; arguments = @('--version') },",
" [pscustomobject]@{ name = 'git'; arguments = @('--version') }",
')',
'',
'function Get-CommandCandidates {',
' param([string]$Name)',
' @(',
' Get-Command -Name $Name -All -ErrorAction SilentlyContinue |',
' ForEach-Object {',
' [ordered]@{',
' commandType = $_.CommandType.ToString()',
' name = $_.Name',
' definition = $_.Definition',
' source = $_.Source',
' version = if ($_.Version) { $_.Version.ToString() } else { $null }',
' }',
' }',
' )',
'}',
'',
'function Get-VersionOutput {',
' param([string]$Name, [string[]]$Arguments)',
' if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {',
" return @('NOT FOUND')",
' }',
' try {',
' $lines = @(& $Name @Arguments 2>&1 | Select-Object -First 3 | ForEach-Object { $_.ToString() })',
" return @($lines + ('exitCode=' + $LASTEXITCODE))",
' } catch {',
" return @('FAILED: ' + $_.Exception.Message)",
' }',
'}',
'',
'$pathEntries = @($env:Path -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ })',
'$report = [ordered]@{',
' formatVersion = 1',
" capturedAt = (Get-Date).ToString('o')",
' powershell = [ordered]@{',
' version = $PSVersionTable.PSVersion.ToString()',
' edition = $PSVersionTable.PSEdition',
' }',
' console = [ordered]@{',
' activeCodePage = ((& cmd.exe /d /c chcp) -join " ").Trim()',
' outputEncoding = [Console]::OutputEncoding.WebName',
' }',
' environment = [ordered]@{',
' pathEntries = $pathEntries',
' pathext = $env:PATHEXT',
' }',
' commands = @(',
' foreach ($check in $toolChecks) {',
' [ordered]@{',
' name = $check.name',
' candidates = Get-CommandCandidates $check.name',
' versionOutput = Get-VersionOutput $check.name $check.arguments',
' }',
' }',
' )',
'}',
'',
'$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutputPath -Encoding UTF8',
"Write-Host ('Written: ' + $OutputPath)",
].join('\n')),
paragraph('В отчёт попадает только заранее выбранный набор полей. Это полезнее, чем <code>Get-ChildItem Env:</code> целиком: там могут оказаться адрес прокси, ключ приложения или служебный путь. Сам <code>PATH</code> тоже способен раскрыть имя локального пользователя и внутренние каталоги. Перед публикацией в задаче или чате открываю JSON, заменяю такие части на нейтральные метки и сохраняю исходник только в закрытом месте.'),
heading('Снимаю отчёт в отдельном сеансе и читаю его как данные'),
paragraph('Для первой проверки полезен PowerShell без профиля: так случайная функция из <code>$PROFILE</code> не выдаст себя за установленный инструмент. Это не отменяет проверку обычной рабочей консоли. Наоборот, если снимки расходятся, профиль становится одной из гипотез, которую можно подтвердить отдельным запуском.'),
codeBlock([
'powershell.exe -NoProfile -File .\\tools\\Capture-Environment.ps1',
'',
'Get-Content .\\tools\\environment-snapshot.json -Raw |',
' ConvertFrom-Json |',
' Format-List formatVersion, powershell, console, environment, commands',
'',
'Get-Command node -All |',
' Select-Object CommandType, Name, Version, Definition |',
' Format-Table -AutoSize',
].join('\n')),
paragraph('Готовый запуск не обязан вернуть все четыре инструмента. Для проекта на PHP отсутствие <code>node</code> может быть нормальным, а отсутствие <code>php</code> — нет. Важен не список «зелёных» строк, а заранее оговорённый набор. Если <code>versionOutput</code> содержит <code>NOT FOUND</code>, я не дописываю путь в системные настройки наугад: сначала смотрю кандидатов и документацию самого проекта.'),
heading('Короткий порядок работы'),
orderedList([
'В README назвать команды проекта и инструменты, которые им нужны: например, <code>php</code>, <code>composer</code>, <code>node</code> и <code>git</code>.',
'Сохранить скрипт в репозитории или рядом с ним, но добавить созданный JSON в ignore, если он содержит локальные пути.',
'Запустить снимок из обычной консоли и, при спорном случае, повторить запуск с <code>-NoProfile</code>.',
'Проверить для каждого инструмента первый кандидат, остальные кандидаты и фактический вывод <code>--version</code>.',
'Перед передачей отчёта убрать имя пользователя, внутренние каталоги и всё, что не требуется для разбора.',
'После правки среды открыть новую консоль, снять новый отчёт и сравнить именно изменившиеся строки.',
]),
heading('Где минимальный снимок не отвечает'),
paragraph('Такой файл не доказывает, что зависимости проекта совпадают. Он не заменяет lock-файл, исходный код, права доступа к каталогу, настройки прокси и архитектуру 32/64 bit. Кодовая страница консоли тоже не равна кодировке каждого файла: файл может быть сохранён в другой кодировке, а программа может читать его своим правилом. Если симптом связан с файлами, отдельно проверяю байты файла и настройки конкретного компилятора, а не объявляю <code>chcp</code> единственной причиной.'),
paragraph('Не стоит хранить снимок годами как истину. После обновления PHP, Node или Git он честно меняется. Его ценность в другом: у команды есть маленькая запись «что именно запускалось в этот день» и воспроизводимый способ обновить её без переустановки всего компьютера.'),
heading('Что считаю готовым'),
paragraph('Задача закрыта, когда новый разработчик может открыть JSON и ответить на три вопроса: какая консоль работала, откуда она взяла нужный бинарник и какую версию тот напечатал. Если один из ответов отсутствует, следующий шаг тоже ясен — дополнить снимок конкретным инструментом, а не обмениваться фразой «у меня работает».'),
sourceList([sources.environment, sources.getCommand, sources.chcp, sources.convertToJson]),
].join('\n'),
};
const mechanismArticle = {
slug: 'editorial-2018-09-mechanism-windows-dev-env',
title: 'Windows. Почему PATH, кодировка и версия бинарника меняют запуск проекта',
categories: ['Windows', 'Инструменты'],
cover: '/assets/editorial/2018/windows-dev-env-resolution-2018.svg',
excerpt: 'Разбираем один механизм: как PowerShell выбирает команду, почему where.exe не заменяет Get-Command -All и где кодовая страница действительно влияет на диагностику.',
readingMinutes: 11,
contentHtml: [
paragraph('В одной консоли <code>node --version</code> показывает ожидаемую версию, в другой — старую, а русское сообщение инструмента превращается в набор знаков. Цена ошибки выше одной неудачной сборки: можно исправить не тот <code>node.exe</code>, сохранить повреждённый текстовый файл или навсегда засорить системный <code>PATH</code> ради разового опыта.'),
paragraph('Разберём один вопрос: <strong>как Windows и PowerShell выбирают бинарник и почему рядом с этим выбором приходится проверять кодировку?</strong> Сначала увидим путь команды в текущем процессе, затем отдельно посмотрим кодовую страницу. Это две связанные проверки, но не одна «настройка окружения».'),
heading('У процесса свой набор переменных'),
paragraph('Переменная среды всегда строка, а дочерний процесс наследует её от родителя. На Windows значения могут существовать в системной, пользовательской и процессной области. Когда мы присваиваем <code>$env:Path</code> в текущем PowerShell, меняется только этот сеанс и программы, запущенные из него. Такой короткий опыт безопаснее постоянной правки: можно поставить папку с нужной версией первой, посмотреть результат и закрыть окно, если гипотеза не подтвердилась.'),
paragraph('Из этого следует простой, но важный вывод. Снимок <code>PATH</code> из нового окна и снимок из уже открытой консоли могут различаться без ошибки Windows. Если правка сделана в настройках пользователя или системы, старое приложение не переписывает свой блок переменных само. Для проверки открываю новую консоль и фиксирую время снимка, а не ожидаю, что одна строка из реестра мгновенно объяснит чужой терминал.'),
figure(
'/assets/editorial/2018/windows-dev-env-resolution-2018.svg',
'Схема выбора команды в Windows PowerShell: процесс получает PATH и PATHEXT, Get-Command -All показывает функции, alias и приложения в порядке выбора, where.exe ищет файлы, а кодовая страница проверяется отдельной веткой.',
'Путь к бинарнику и способ отображения его сообщения надо проверять отдельно: один отвечает за запуск, второй — за читаемость вывода.',
),
heading('PATH и PATHEXT отвечают только за часть выбора'),
paragraph('<code>PATH</code> — это список каталогов, где система ищет исполняемые файлы. В Windows его элементы разделены точкой с запятой. <code>PATHEXT</code> задаёт расширения, которые считаются исполняемыми, поэтому имя <code>tool</code> может привести к <code>tool.exe</code> или <code>tool.cmd</code>. Но для PowerShell этого мало: перед приложением может оказаться alias, функция, cmdlet или внешний скрипт с тем же именем.'),
paragraph('Поэтому <code>where.exe node</code> полезен, но не является окончательным ответом. Он ищет файлы в текущем каталоге и папках <code>PATH</code>; он не покажет функцию <code>node</code>, определённую в профиле PowerShell. <code>Get-Command node -All</code> выводит все варианты в порядке, который PowerShell использует при выборе. В диагностике я запускаю обе команды: первая хорошо показывает физические файлы, вторая — фактическую команду оболочки.'),
codeBlock([
'$names = @("node", "npm", "php", "git")',
'',
'foreach ($name in $names) {',
' Write-Host ""',
' Write-Host ("== " + $name + " ==")',
' Get-Command -Name $name -All -ErrorAction SilentlyContinue |',
' Select-Object CommandType, Name, Version, Source, Definition |',
' Format-Table -AutoSize',
'',
' Write-Host "-- files found by where.exe --"',
' where.exe $name 2>$null',
'',
' if (Get-Command -Name $name -ErrorAction SilentlyContinue) {',
' & $name --version',
' Write-Host ("exitCode=" + $LASTEXITCODE)',
' }',
'}',
].join('\n')),
paragraph('У этой проверки есть ограничение: аргумент <code>--version</code> подходит не каждой утилите. Для старого проекта список аргументов лучше хранить рядом с проектом, а не использовать универсальный запуск. Если команда является функцией, её вывод ещё не доказывает путь к приложению. В таком случае сначала называю тип кандидата, затем проверяю его определение и решаю, является ли это частью проекта или случайной надстройкой профиля.'),
heading('Кодовая страница — отдельный слой'),
paragraph('<code>chcp</code> показывает активную кодовую страницу консоли и умеет её менять. Документация Windows отдельно оговаривает, что программы, запущенные после смены страницы, используют новое значение, а уже запущенные программы обычно сохраняют прежнее. Это объясняет частый ложный вывод «я поменял кодировку, но ошибка осталась»: проверка сделана в процессе, который стартовал раньше.'),
paragraph('Для разбора не надо немедленно переключать консоль на 65001 или менять системный язык. Сначала записываю три наблюдения: строку <code>chcp</code>, <code>[Console]::OutputEncoding.WebName</code> и байты проблемного файла, если проблема именно в файле. Кодовая страница <code>cmd.exe</code>, настройка вывода .NET и кодировка файла — разные вещи. Совпадение двух из них ничего не гарантирует, а изменение одного не лечит остальные.'),
codeBlock([
'# Наблюдение, без изменения глобальных настроек.',
'& cmd.exe /d /c chcp',
'[Console]::OutputEncoding.WebName',
'[Text.Encoding]::Default.WebName',
'',
'# Короткий опыт только в текущем PowerShell.',
'$projectTools = Join-Path $PSScriptRoot "tools"',
'$env:Path = $projectTools + ";" + $env:Path',
'',
'Get-Command node -All |',
' Select-Object CommandType, Name, Version, Definition |',
' Format-Table -AutoSize',
'node --version',
].join('\n')),
paragraph('Последние четыре строки намеренно меняют только процессную область. Если нужный <code>node.exe</code> оказался первым и проект прошёл документированную команду, гипотеза подтверждена. Если нет, закрываю окно — постоянного следа не осталось. Лишь после такой проверки имеет смысл обсуждать установщик, путь в пользовательском <code>PATH</code> или явный путь в скрипте проекта.'),
heading('Как не спутать три похожих симптома'),
dataTable(
['Наблюдение', 'Что оно означает', 'Чем проверить', 'Следующее действие'],
[
['<code>node --version</code> даёт старую версию', 'Выбран другой кандидат или путь в <code>PATH</code> стоит раньше', '<code>Get-Command node -All</code> и <code>where.exe node</code>', 'Временно поставить нужную папку первой в текущем сеансе и повторить версию'],
['<code>where.exe</code> показывает два файла, а PowerShell запускает функцию', 'Физические файлы не описывают приоритет PowerShell', '<code>Get-Command node -All</code> с колонкой <code>CommandType</code>', 'Проверить профиль и не менять <code>PATH</code>, пока функция не исключена'],
['Русский вывод нечитаем только в одной консоли', 'Различается кодовая страница или настройка вывода', '<code>chcp</code> и <code>[Console]::OutputEncoding</code>', 'Сравнить новую консоль и конкретный способ записи файла'],
['После правки переменной результат прежний', 'Работает старый процесс с унаследованными значениями', 'Снимок из нового PowerShell без профиля', 'Открыть новый сеанс и повторить одну исходную команду'],
['Сборка читает верный Node, но падает дальше', 'Причина не в поиске бинарника', 'Lock-файл, лог команды, права и конфигурация проекта', 'Не продолжать править <code>PATH</code>; расследовать следующий симптом'],
],
),
heading('Последовательность проверки'),
orderedList([
'Зафиксировать одну воспроизводимую команду проекта и её полный текст ошибки, не меняя настройки заранее.',
'В той же консоли вывести <code>Get-Command -All</code>, <code>where.exe</code> и <code>--version</code> для нужного инструмента.',
'Открыть новый PowerShell с <code>-NoProfile</code> и повторить ровно те же три наблюдения.',
'Если расходится путь, добавить нужный каталог только в <code>$env:Path</code> текущего сеанса и проверить исходную команду.',
'Если расходится текст, записать <code>chcp</code>, настройку вывода и кодировку конкретного файла, не подменяя одну проверку другой.',
'Постоянное изменение делать лишь после того, как временный опыт дал понятный результат и его можно описать в README.',
]),
heading('Границы такого объяснения'),
paragraph('Механизм выбора команды не объясняет всё. Бинарники могут отличаться разрядностью, зависимыми DLL, правами запуска, содержимым каталога или настройками антивируса и прокси. Эти ограничения не повод отключать защиту и начинать с переустановки Windows. Они означают, что после совпадения пути и версии нужно смотреть следующий наблюдаемый факт — лог приложения, разрешения каталога или сетевой запрос.'),
paragraph('Так же осторожно отношусь к кодировке. <code>chcp</code> нужен для проверки активной консоли, но не является рецептом «всегда ставить UTF-8». У старых программ и редакторов могут быть свои ожидания. Воспроизводимым считается результат, когда одна и та же программа из новой консоли выдаёт читаемый текст и открывает нужные файлы по документированному правилу.'),
heading('Итог механизма'),
paragraph('Имя команды не равно конкретному файлу. В Windows PowerShell между ними стоят процессное окружение, порядок <code>PATH</code>, <code>PATHEXT</code> и приоритет alias, функций, cmdlet и приложений. Рядом живёт отдельный слой отображения текста. Когда эти два пути проверены отдельными командами, «на моей машине другая версия» превращается в строку, которую можно сравнить и исправить без гадания.'),
sourceList([sources.environment, sources.processEnvironment, sources.getCommand, sources.where, sources.chcp]),
].join('\n'),
};
const fieldArticle = {
slug: 'editorial-2018-09-field-windows-dev-env',
title: 'Windows. Разбор «работает на моей машине» без переустановки среды',
categories: ['Windows', 'Инструменты'],
cover: '/assets/editorial/2018/windows-dev-env-diff-2018.svg',
excerpt: 'Полевой маршрут для двух Windows-машин: сравнить снимки, отделить PATH от кодировки и версии бинарника, проверить одну гипотезу в новом сеансе и не стереть полезные доказательства.',
readingMinutes: 12,
contentHtml: [
paragraph('Симптом: коллега присылает фразу «на моей машине работает», а на второй Windows-машине та же команда падает или собирает другой результат. Цена поспешного ответа — потерянный след: переустановка инструмента, очистка каталогов и правка глобального <code>PATH</code> меняют несколько переменных сразу и уже не дают понять, что было причиной.'),
paragraph('Разберём один вопрос: <strong>как найти различие между двумя Windows-окружениями, не превращая диагностику в серию случайных установок?</strong> Ниже учебный разбор. Пути и версии в нём условные, а не рассказ о чужом компьютере; важен порядок сбора доказательств и одна обратимая проверка за раз.'),
heading('Сначала делаю два запуска сравнимыми'),
paragraph('До снимка фиксирую одинаковые входные данные: один commit проекта, одинаковую команду, наличие lock-файла и каталог, из которого она запущена. Если на одной машине уже изменён <code>package-lock.json</code> или добавлены локальные правки, сравнение среды смешается со сравнением проекта. В этом случае сначала сохраняю статус VCS и называю различие, а затем возвращаюсь к окружению.'),
paragraph('На обеих машинах запускаю один и тот же <code>Capture-Environment.ps1</code> из предыдущей заметки. Две копии JSON называю <code>machine-a.json</code> и <code>machine-b.json</code>. Перед отправкой в общий канал убираю личные части путей. Нам не нужен полный профиль пользователя; для расследования нужны порядок папок, кандидаты команд, версия PowerShell и признаки консоли.'),
figure(
'/assets/editorial/2018/windows-dev-env-diff-2018.svg',
'Диагностический путь для двух Windows-машин: одинаковая команда и commit, два очищенных снимка окружения, сравнение PATH и кандидатов бинарника, временный опыт в новом PowerShell, затем повтор исходной команды.',
'Сравнение не выбирает настройку за человека. Оно сужает вопрос до одного наблюдаемого различия и оставляет обратимый шаг проверки.',
),
heading('Учебный пример: один репозиторий, два кандидата node'),
paragraph('Представим, что на машине A <code>Get-Command node -All</code> первым показывает <code>C:\\Tools\\node-6\\node.exe</code>, а на машине B — <code>C:\\Program Files\\nodejs\\node.exe</code>. В JSON это не просто две строки с разными цифрами. На машине A первая папка из <code>PATH</code> раньше; на машине B этой папки нет. Пока не известно, какую версию требует проект, нельзя объявить один компьютер «правильным» и переносить его путь на второй.'),
dataTable(
['Поле сравнения', 'Машина A в учебном примере', 'Машина B в учебном примере', 'Что это доказывает'],
[
['Первый кандидат <code>node</code>', '<code>C:\\Tools\\node-6\\node.exe</code>', '<code>C:\\Program Files\\nodejs\\node.exe</code>', 'PowerShell может запускать разные файлы при одинаковом имени команды'],
['Позиция каталога в <code>PATH</code>', '<code>C:\\Tools\\node-6</code> стоит раньше', 'Этого каталога нет', 'Различие объясняет порядок поиска, но ещё не требуемую версию проекта'],
['<code>node --version</code>', 'Условно <code>v6.x</code>', 'Условно <code>v8.x</code>', 'Версия должна быть зафиксирована рядом с зависимостями проекта'],
['Кодовая страница', 'Например, один текст <code>chcp</code>', 'Другая строка <code>chcp</code>', 'Нечитаемый вывод может потребовать отдельной проверки, но не меняет путь к бинарнику'],
['Исходная команда', 'Падает или даёт другой лог', 'Работает в учебном случае', 'Это повод проверить одну гипотезу, а не удалить всё окружение A'],
],
),
heading('Превращаю JSON в сравнимые строки'),
paragraph('В снимке есть время создания и путь каждого кандидата, поэтому сырой текст файлов неудобно сравнивать целиком. Небольшая функция ниже выбирает поля, которые относятся к запуску. Она не скрывает порядок <code>PATH</code>: каждая папка остаётся отдельной строкой. Если на машине не найден инструмент, функция записывает это явно вместо пустого объекта.'),
codeBlock([
'# tools/Compare-Environment.ps1',
'function Get-SnapshotLines {',
' param([string]$SnapshotPath)',
'',
' $snapshot = Get-Content -LiteralPath $SnapshotPath -Raw | ConvertFrom-Json',
' $lines = @(',
' "powershell=" + $snapshot.powershell.version',
' "consoleCodePage=" + $snapshot.console.activeCodePage',
' "outputEncoding=" + $snapshot.console.outputEncoding',
' "pathext=" + $snapshot.environment.pathext',
' )',
'',
' foreach ($entry in @($snapshot.environment.pathEntries)) {',
' $lines += "path=" + $entry',
' }',
'',
' foreach ($tool in @($snapshot.commands)) {',
' $first = @($tool.candidates | Select-Object -First 1)',
' if ($first.Count -eq 0) {',
' $lines += $tool.name + "=NOT FOUND"',
' continue',
' }',
' $candidate = $first[0]',
' $lines += ("{0}={1}|{2}|{3}" -f $tool.name, $candidate.commandType, $candidate.definition, $candidate.version)',
' }',
'',
' return $lines',
'}',
'',
'$machineA = Get-SnapshotLines .\\machine-a.json',
'$machineB = Get-SnapshotLines .\\machine-b.json',
'Compare-Object -ReferenceObject $machineA -DifferenceObject $machineB',
].join('\n')),
paragraph('<code>Compare-Object</code> показывает строки, существующие только в одной из сторон. Его стрелки не являются диагнозом: они лишь говорят, в каком файле встретилась строка. Сначала смотрю пару «первый кандидат инструмента + соответствующая папка <code>PATH</code>», затем проверяю версию командой. Если отличаются десять строк, не исправляю все десять. Выбираю различие, которое прямо связано с исходной ошибкой.'),
heading('Проверяю гипотезу в новом сеансе'),
paragraph('Допустим, README проекта требует конкретную версию Node, а учебный снимок A показывает старый бинарник раньше нужного. Сначала открываю новый PowerShell и временно добавляю каталог с нужной версией в начало <code>$env:Path</code>. Это действие влияет только на текущий процесс и всё, что будет запущено из него. Если исходная команда не изменилась, гипотеза про порядок <code>PATH</code> не подтвердилась — возвращаюсь к логу, а не переношу путь в системные переменные.'),
codeBlock([
'# Новый PowerShell. Путь взят из документации проекта, не из случайного каталога.',
'$requiredNode = "C:\\Tools\\node-8"',
'if (-not (Test-Path (Join-Path $requiredNode "node.exe"))) {',
' throw "В каталоге нет node.exe: $requiredNode"',
'}',
'',
'$env:Path = $requiredNode + ";" + $env:Path',
'Get-Command node -All |',
' Select-Object CommandType, Name, Version, Definition |',
' Format-Table -AutoSize',
'node --version',
'',
'# Только после этих строк повторяется исходная команда проекта.',
'npm run build',
].join('\n')),
paragraph('Этот пример не предлагает брать <code>C:\\Tools\\node-8</code> из статьи. Каталог и версия должны быть определены проектом или его официальной документацией. Если файл не найден, скрипт останавливается с понятной ошибкой. Он не скачивает исполняемый файл, не меняет <code>PATH</code> на уровне пользователя или системы и не требует отключать политику запуска сценариев.'),
heading('Как отделяю PATH, кодировку и версию'),
paragraph('Три различия часто приходят вместе, но их нельзя лечить одним действием. Старый бинарник проявляется через путь и <code>--version</code>. Кодовая страница проявляется через нечитаемый вывод и значения <code>chcp</code> или <code>OutputEncoding</code>. Версия зависимостей проекта проявляется через lock-файл и лог пакетного менеджера. Если меняется только текст сообщения, а путь и версия совпали, спор про <code>PATH</code> стоит прекратить.'),
dataTable(
['Гипотеза', 'Минимальное доказательство', 'Обратимое действие', 'Когда остановиться'],
[
['В <code>PATH</code> раньше старая папка', 'Первый кандидат и первая отличающаяся строка пути', 'Добавить документированный каталог только в <code>$env:Path</code> нового PowerShell', 'Если версия или исходная ошибка не изменились'],
['Профиль подменяет команду', '<code>Get-Command -All</code> показывает функцию или alias', 'Сравнить запуск с <code>-NoProfile</code>', 'Если тип кандидата остаётся <code>Application</code> и путь тот же'],
['Консоль портит сообщение', 'Различаются <code>chcp</code> и настройка вывода при одинаковом бинарнике', 'Открыть новую консоль и проверить вывод одной команды', 'Если проблема относится к сохранённому файлу, а не к консоли'],
['Проект требует другую версию', 'README или конфигурация проекта прямо называет версию, а <code>--version</code> не совпадает', 'Использовать одобренную поставку этой версии в отдельном сеансе', 'Если после совпадения версии ошибка переходит к другой причине'],
],
),
heading('Порядок полевого разбора'),
orderedList([
'Сохранить commit, lock-файл и точный запуск, который расходится на двух машинах.',
'Снять очищенные отчёты из одинакового PowerShell и не публиковать в них секреты или персональные пути.',
'Сравнить PowerShell, кодовую страницу, порядок <code>PATH</code>, <code>PATHEXT</code>, первого кандидата и вывод версии.',
'Выбрать одно отличие, напрямую связанное с ошибкой, и проверить его в новом сеансе только через процессное <code>$env:Path</code>.',
'Повторить исходную команду и сохранить новый снимок, если результат изменился.',
'Лишь после подтверждения обновить README или согласованный способ установки; ненужные глобальные правки не оставлять.',
]),
heading('Что останется вне этого разбора'),
paragraph('Два одинаковых снимка не гарантируют одинаковую сборку. Причина может жить в правах на каталог, сертификате прокси, сетевом доступе, разрядности, нативной библиотеке, антивирусе, содержимом кеша или строках окончания файла. Эти варианты проверяются следующими отдельными наблюдениями. Нельзя делать вывод, что защита мешает проекту, и отключать её без подтверждённой причины и разрешённого процесса.'),
paragraph('Также не стоит превращать учебный дифф в обязательный корпоративный формат. Для маленького проекта достаточно текстового снимка и одной команды сравнения. Главное — чтобы следующий человек мог повторить путь: увидеть исходную ошибку, сравнить один набор полей, провести обратимый опыт и сказать, что именно изменилось.'),
heading('Итог: вместо «у меня работает»'),
paragraph('Фраза становится полезной, когда к ней приложены три вещи: команда, снимок процесса и сравнение кандидата бинарника. Тогда диагностика не начинается с переустановки. Мы сначала видим отличие, проверяем его в новом сеансе и только затем решаем, нужно ли менять постоянную настройку. В большинстве локальных случаев этого достаточно, чтобы вернуть разговор от мнений к наблюдаемым данным.'),
sourceList([sources.environment, sources.getCommand, sources.chcp, sources.compareObject, sources.where]),
].join('\n'),
};
export const revisions = [practiceArticle, mechanismArticle, fieldArticle];
const isDirectExecution = Boolean(process.argv[1])
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isDirectExecution) {
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
} else {
process.stderr.write('Usage: node web/scripts/upgrade-2018-09.mjs --print-revisions\n');
}
}