Как проверить локальные ссылки в Markdown после правки ИИ в Windows
Составляем явный список относительных путей и проверяем наличие файлов. С учебным примером, скриптом Node.js и границами проверки.
Подробности проверки
- Windows 10.0.26300
- PowerShell 7.6.5
- Node.js v24.22.1
После правки ИИ ссылка может вести к переименованному файлу или отсутствующей картинке. Ниже — проверка существования файлов из явного списка путей. Вы вручную переносите адреса из одного Markdown-файла, а скрипт проверяет их внутри выбранной папки. Он не разбирает Markdown и не подтверждает исправность всего документа или проекта.
Что понадобится
Нужны Windows, PowerShell и Node.js 24.x. Проверьте версию командой ниже. Если node не найден, получите Windows-версию ветки 24.x с официальной страницы https://nodejs.org/en/download, следуйте официальным инструкциям установки и откройте новый PowerShell. Покупки, npm-пакеты и платный API не нужны. Установка здесь не автоматизируется.
node --versionОжидается v24.x.x. Используйте обычную локальную папку, не UNC-путь, сетевую папку, symlink или junction. Не меняйте файлы во время проверки. Скрипт отклоняет обнаруженные символические ссылки и junction в пути, включая предков корня. Это не защита от одновременной подмены файлов другим процессом.
1. Подготовьте учебный пример

- Создайте новую пустую папку md-link-lab командами ниже. Если имя занято, выберите другое и замените его во всех командах; не выполняйте пример поверх своих документов.
- Создайте учебные файлы. picture.png намеренно содержит текст: проверяется наличие файла, а не корректность изображения.
- Сохраните весь следующий JavaScript-код как check-files.mjs рядом с md-link-lab, в UTF-8.
New-Item -ItemType Directory -Path .\md-link-lab -ErrorAction Stop | Out-Null
New-Item -ItemType Directory -Path .\md-link-lab\docs | Out-Null
Set-Content -Encoding utf8 .\md-link-lab\docs\intro.md 'Учебный текст'
Set-Content -Encoding utf8 '.\md-link-lab\docs\with space.md' 'Учебный текст'
Set-Content -Encoding utf8 .\md-link-lab\picture.png 'fixture, not a real image'
Set-Content -Encoding utf8 .\md-link-lab\README.md @('[Введение](docs/intro.md)', '[Пробел](docs/with%20space.md)', '', '[Нет файла](docs/missing.md)')
Set-Content -Encoding utf8 .\md-link-lab\links.json '["docs/intro.md", "docs/with%20space.md", "picture.png", "docs/missing.md"]'import { lstatSync, readFileSync } from 'node:fs';
import { resolve, parse, join, relative, isAbsolute, sep } from 'node:path';
function noLinks(absolute) {
let current = parse(absolute).root;
const parts = absolute.slice(current.length).split(sep).filter(Boolean);
if (lstatSync(current).isSymbolicLink()) throw Error('symlink-root');
for (const part of parts) {
current = join(current, part);
if (lstatSync(current).isSymbolicLink()) throw Error('symlink-or-junction');
}
return lstatSync(absolute);
}
function localFile(root, raw) {
if (typeof raw !== 'string' || !raw || raw.length > 1000) throw Error('needs-manual: expected path');
// Deliberately small path language; this is not a Markdown parser.
const value = raw.replaceAll('%20', ' ');
if (/[\\:%?#<>|"*\x00-\x1f\x7f]/u.test(value) || value.startsWith('/'))
throw Error('needs-manual: unsupported path');
const parts = value.split('/');
if (parts.some(p => !p || p === '.' || p === '..' || /[. ]$/u.test(p)
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(p)))
throw Error('needs-manual: unsupported segment');
const target = resolve(root, ...parts);
const rel = relative(root, target);
if (!rel || rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel))
throw Error('needs-manual: outside-root');
if (!noLinks(target).isFile()) throw Error('needs-manual: not a regular file');
return target;
}
try {
if (process.argv.length !== 4) throw Error('usage: node check-files.mjs ROOT LIST-RELATIVE.json');
const root = resolve(process.argv[2]);
if (process.platform !== 'win32' || !/^[a-z]:\\/iu.test(root)) throw Error('local Windows drive required; no UNC');
if (!noLinks(root).isDirectory()) throw Error('root must be directory');
const listFile = localFile(root, process.argv[3]);
const list = JSON.parse(readFileSync(listFile, 'utf8').replace(/^\uFEFF/u, ''));
if (!Array.isArray(list) || list.length < 1 || list.length > 500)
throw Error('needs-manual: list must have 1..500 paths');
let failed = 0;
for (const item of list) {
try {
localFile(root, item);
console.log('EXISTS', JSON.stringify(item));
} catch (error) {
failed++;
console.log(error.code === 'ENOENT' ? 'MISSING' : 'NEEDS-MANUAL', JSON.stringify(item), error.code || error.message);
}
}
console.log(failed ? 'LIST-HAS-ISSUES' : 'FILES-EXIST-IN-LIST');
console.log('NEEDS-MANUAL: Markdown syntax, list completeness, anchors and remote URLs were not checked.');
process.exitCode = failed ? 1 : 0;
} catch (error) {
console.error('NEEDS-MANUAL', error.code || error.message);
process.exitCode = 2;
}
2. Запустите проверку и сравните хеши
- Перед запуском снимите SHA-256 учебных файлов. Команды ниже хранят снимок в переменной PowerShell; файл отчёта в проверяемую папку не записывается.
- Запустите node с корнем папки и именем списка относительно корня. Сразу сохраните код возврата в переменную.
- Повторите хеширование. Пустой вывод Compare-Object означает, что пути и хеши файлов совпали между двумя снимками. Это результат конкретного сравнения, а не гарантия будущей неизменности.
$before = @(Get-ChildItem -LiteralPath .\md-link-lab -Recurse -File | Get-FileHash -Algorithm SHA256 | ForEach-Object { "$($_.Path)|$($_.Hash)" } | Sort-Object)
node .\check-files.mjs .\md-link-lab links.json
$checkExit = $LASTEXITCODE
$after = @(Get-ChildItem -LiteralPath .\md-link-lab -Recurse -File | Get-FileHash -Algorithm SHA256 | ForEach-Object { "$($_.Path)|$($_.Hash)" } | Sort-Object)
Compare-Object -ReferenceObject $before -DifferenceObject $after
$checkExitДля примера ожидаются три EXISTS, один MISSING для docs/missing.md, итог LIST-HAS-ISSUES и код 1. В конце всегда появляется NEEDS-MANUAL: сам Markdown и полнота списка не проверялись. Это ожидаемый исход с намеренно отсутствующим файлом, а не ошибка установки.
Для второго исхода измените в учебном README.md адрес docs/missing.md на docs/intro.md и так же исправьте последний элемент links.json. Затем заново выполните весь блок со снимками хешей. Ожидается FILES-EXIST-IN-LIST и код 0. Правки внесены до нового снимка; сам скрипт их не делает.
3. Перенесите метод на копию Markdown
- Возьмите отдельную локальную копию папки. Корнем выберите папку, где лежит проверяемый Markdown-файл. Для Markdown в подпапке используйте отдельный корень и список.
- Прочитайте Markdown как текст. Из [Инструкция](docs/intro.md) перенесите в JSON только docs/intro.md; аналогично выпишите пути изображений. ИИ может предложить список, но человек должен сверить каждую запись и убедиться, что нужные ссылки не пропущены.
- Пишите пути через /, JSON-строки — в двойных кавычках. Допускаются пробелы и %20. Начальное ./ уберите при переносе в список. Исходный Markdown автоматически не меняется.
- Запустите тот же check-files.mjs с новым корнем и списком. Разберите все MISSING и NEEDS-MANUAL. Исправляйте адрес в копии Markdown и согласованно обновляйте список; затем повторяйте запуск.
Как читать отчёт

| Результат | Значение |
|---|---|
| EXISTS | Сейчас обнаружен обычный файл. Содержимое, смысл ссылки и корректность изображения не проверены. |
| MISSING | Один из компонентов пути отсутствует. Проверьте опечатку, переименование и корень. |
| NEEDS-MANUAL | Строка требует ручной проверки. Не считайте её успешной. |
| FILES-EXIST-IN-LIST; код 0 | Все элементы списка ведут к существующим обычным файлам. Это не PASS Markdown. |
| LIST-HAS-ISSUES; код 1 | Есть отсутствующие файлы или неподдерживаемые элементы. |
| Код 2 | Проверка не началась: проблема запуска, корня, JSON или файла списка. |
Что остаётся проверить вручную
Ссылки [текст][id], определения ссылок, сокращённые ссылки, вложенные скобки, необязательные заголовки, HTML и экранирование требуют ручного разбора. Не включайте примеры из блоков кода и обратных кавычек как действующие ссылки. Скрипт Markdown вообще не читает: эти случаи всегда остаются NEEDS-MANUAL, даже при коде 0. Он не проверяет полноту списка, внешние URL, якоря заголовков, регистр для будущего Linux-сервера или результат публикации сайта.
URL, якоря #section, query-параметры, абсолютные пути, обратные слеши, переходы .., каталоги и пути через symlink/junction в списке отклоняются, а не молча пропускаются. Кроме %20, percent-кодирование не поддерживается. Скрипт не обходит подпапки и не ищет ссылки за вас. Файл может существовать, но оказаться не тем документом: важные адресаты откройте и проверьте отдельно.
Отмена и завершение
Проверяющий читает JSON-список и метаданные путей, не исправляет Markdown и не записывает проверяемые файлы. Автоматически откатывать ему нечего. Собственные правки отменяйте по исходной копии. Учебную папку и check-files.mjs можно удалить, убедившись, что это именно созданный вами пример. Перед передачей документа ещё раз сверьте Markdown со списком: успешная проверка неполного списка остаётся неполной проверкой.