/
ump-team
/
ump-infra
Обзор
Документация
Войти
/
ump-team
/
ump-infra
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev/test
scripts/validate_md.py
683 строки
34 KB
Dmitry Kochenov
feat: масштабный рефакторинг UMP-платформы, переработка документации и новые агенты-воркфлоу
09 авг 2026, 08:23
09 авг 2026, 08:23
47c8939
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ Валидатор MD-файлов для Universal Modular Platform. Запуск: uv run python scripts/validate_md.py <path-to-md-file> uv run python scripts/validate_md.py docs/01-setup/step-1-0.md uv run python scripts/validate_md.py --all # проверить все .md в docs/ Exit 0 = VALID, Exit 1 = INVALID. Проверяет (правила M01–M15): M01 — ровно один H1 заголовок (#) в начале файла M02 — все тройные бэктики ``` сбалансированы (чётное количество) M03 — все таблицы имеют разделитель |---| после строки заголовка M04 — во всех строках таблицы одинаковое количество колонок | M05 — нет незакрытых угловых плейсхолдеров <...> вне код-блоков M06 — нет английских разговорных фраз в русской прозе (вне код-блоков и таблиц) M07 — перед и после каждой строки заголовка # есть пустая строка M08 — для Phase 1 docs_target: присутствует маркер "> Phase 1 — инструкция для AI-агента" M09 — для Phase 2 docs_target: присутствует маркер "> Обучающая документация" M10 — для Phase 2: присутствуют все обязательные секции M11 — для Phase 2: отсутствует маркер Phase 1 M12 — для Phase 2: VERIFIED_CHECKLIST имеет имя "## VERIFIED_CHECKLIST" (не "## Критерии готовности (...)") M13 — для Phase 2: все пункты VERIFIED_CHECKLIST закрыты [x] (не [ ]) M14 — нет TODO/FIXME/XXX/placeholder вне код-блоков M15 — нет literal escape-последовательностей \n, \t, \" вне код-блоков Контекст: Ранее выявлены системные баги в .agent/prompts/ и .agent/rules/ форматирования MD-файлов, которые приводят к: - сломанным таблицам (несогласованные колонки) - незакрытым код-блокам (весь последующий контент становится кодом) - оставшимся плейсхолдерам <...> в финальной документации - гибридным словам (официальная → 官方ная) - потере сохраняемых секций при переходе Phase 1 → Phase 2 Этот скрипт — автоматизированная проверка, дополняющая step-finalizer.md §1.9. """ import re import sys from pathlib import Path # --- Конфигурация --- # Английские разговорные фразы, запрещённые в русской прозе # (коды статуса OK/FAIL/PASS/PASSED/SKIP разрешены — см. 00-agent-protocol.md §2.0 п.7) FORBIDDEN_ENGLISH_PHRASES: list[str] = [ r'\bLet me\b', r"\bI'll\b", r'\bI will\b', r'\bDone\b', r'\bSuccess\b', r'\bFailed\b', r'\bRun command\?\b', r'\bApprove\?\b', r'\bShould I continue\?\b', r'\bSure\b', r'\bOf course\b', r"\bHere's\b", r"\bLet's\b", ] # Запрещённые плейсхолдеры (вне код-блоков) FORBIDDEN_PLACEHOLDERS: list[str] = [r'TODO', r'FIXME', r'XXX', r'placeholder'] # Гибридные/несуществующие слова (официальная → 官方ная и подобные) FORBIDDEN_HYBRID_WORDS: list[str] = [ '官方ная', '官方ной', '官方ную', # гибрид китайского и русского ] # Обязательные секции Phase 2 (step-finalizer.md §1.2) PHASE2_REQUIRED_SECTIONS: list[str] = [ '## Что вы получите в конце шага', '## Предварительные требования', '## Почему именно так', '## Пошаговая инструкция', '## Как проверить', '## Типичные ошибки и их решения', '## Что дальше', ] # Сохраняемые секции (Phase 1 → Phase 2, не должны меняться) PRESERVED_SECTIONS: list[str] = [ '## История действий агента', '## Реестр файлов шага', '## Реестр тестов шага', '## VERIFIED_CHECKLIST', ] # Старое (неправильное) имя секции VERIFIED_CHECKLIST OLD_VERIFIED_NAME = '## Критерии готовности (VERIFIED_CHECKLIST)' # P2-4 fix (mypy): type-аннотации для Issue и его полей. SplitLine = tuple[bool, str] class Issue: def __init__(self, rule: str, line: int, col: int, msg: str, severity: str = 'ERROR') -> None: self.rule = rule self.line = line self.col = col self.msg = msg self.severity = severity # ERROR or WARN def __str__(self) -> str: loc = f'{self.line}' if self.col: loc += f':{self.col}' icon = '❌' if self.severity == 'ERROR' else '⚠️' return f' {icon} {self.rule:5} [{loc}] {self.msg}' def split_code_blocks(lines: list[str]) -> list[SplitLine]: """Разделяет строки на (in_code_block, line) пары. Возвращает список кортежей (is_in_code_block: bool, line: str). Учитывает только тройные бэктики ``` (не одиночные `). """ result: list[SplitLine] = [] in_code = False for line in lines: # Подсчёт тройных бэктиков в строке (вне экранированных) # Простая эвристика: считаем ``` не внутри одинарных/двойных кавычек stripped = line # Удаляем inline-код `...` чтобы не путать с ``` # (грубая аппроксимация — для большинства случаев достаточно) count = stripped.count('```') if count % 2 == 1: result.append((in_code, line)) in_code = not in_code else: result.append((in_code, line)) return result # ============================================================================ # P1-13 fix: декомпозиция validate_file на отдельные функции по правилам. # Ранее validate_file имел цикломатическую сложность F (135) — все 11 проверок # (M01, M02, M03, M04, M05, M06, M07, M14, M15, Phase2 блок) в одной функции. # Теперь каждая проверка — отдельная функция со сложностью 5-20. # ============================================================================ def _check_m01_h1( lines: list[str], split_lines: list[SplitLine], filepath: Path, frontmatter_end: int, ) -> list[Issue]: """M01: ровно один H1 заголовок (#) в начале файла. Подсчитывает H1 только ВНЕ код-блоков. Пропускает YAML frontmatter. VitePress layout: home — исключение (H1 не обязателен). """ issues: list[Issue] = [] is_vitepress_home = False try: with open(filepath, encoding='utf-8') as f: first_3_lines = [f.readline().strip() for _ in range(3)] if first_3_lines[0] == '---' and any('layout: home' in line for line in first_3_lines): is_vitepress_home = True except OSError: pass h1_lines: list[tuple[int, str]] = [] for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue if i + 1 <= frontmatter_end: continue # пропускаем frontmatter if line.startswith('# ') and not line.startswith('## '): h1_lines.append((i + 1, line)) if is_vitepress_home: # Для VitePress home layout H1 не обязателен (вся структура через hero/features) return issues if len(h1_lines) == 0: issues.append(Issue('M01', 0, 0, 'Отсутствует H1 заголовок (#) в файле (вне код-блоков и frontmatter)')) elif len(h1_lines) > 1: for ln, line in h1_lines: issues.append(Issue('M01', ln, 0, f'Найден повторный H1: {line[:60]}')) elif h1_lines[0][0] != frontmatter_end + 1: # H1 не сразу после frontmatter — допустимо только если перед ним пустые строки h1_line_num = h1_lines[0][0] non_empty_before: list[int] = [] for i in range(frontmatter_end, h1_line_num - 1): if lines[i].strip(): non_empty_before.append(i + 1) if non_empty_before: issues.append(Issue('M01', h1_line_num, 0, f'H1 не сразу после frontmatter (перед ним контент на строках {non_empty_before})')) return issues def _check_m02_backticks(content: str, split_lines: list[SplitLine]) -> list[Issue]: """M02: балансировка тройных бэктиков (чётное количество).""" issues: list[Issue] = [] total_backticks = content.count('```') if total_backticks % 2 != 0: # Найти последнюю незакрытую строку last_open: int | None = None in_code = False for i, (_, line) in enumerate(split_lines): if line.count('```') % 2 == 1: if not in_code: last_open = i + 1 in_code = True else: in_code = False loc = last_open if last_open else 0 issues.append(Issue('M02', loc, 0, f'Незакрытый код-блок: всего ``` = {total_backticks} (нечётное). ' f'Весь контент после последнего открытого ``` становится кодом.')) return issues def _collect_table_blocks(split_lines: list[SplitLine]) -> list[tuple[int, list[tuple[int, str]]]]: """Собрать блоки строк-таблиц (последовательные строки, начинающиеся с |).""" table_blocks: list[tuple[int, list[tuple[int, str]]]] = [] current_block: list[tuple[int, str]] = [] current_block_start = 0 for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: if current_block: table_blocks.append((current_block_start, current_block)) current_block = [] continue stripped = line.strip() if stripped.startswith('|') and stripped.endswith('|'): if not current_block: current_block_start = i + 1 current_block.append((i + 1, stripped)) else: if current_block: table_blocks.append((current_block_start, current_block)) current_block = [] if current_block: table_blocks.append((current_block_start, current_block)) return table_blocks def _check_m03_m04_tables(split_lines: list[SplitLine]) -> list[Issue]: """M03: разделитель |---| после строки заголовка; M04: одинаковое количество колонок.""" issues: list[Issue] = [] inline_code_re = re.compile(r'`[^`]+`') table_blocks = _collect_table_blocks(split_lines) for start_line, block in table_blocks: if len(block) < 2: # Таблица из одной строки — нет разделителя issues.append(Issue('M03', start_line, 0, f'Таблица из 1 строки (нет разделителя |---|): {block[0][1][:50]}')) continue # M03: вторая строка должна быть разделителем |---| second_line = block[1][1] # Разделитель: |---|, |:---|, |---:|, |:---:|, с пробелами if not re.match(r'^\|[\s\-:|]+\|$', second_line): issues.append(Issue('M03', block[1][0], 0, f'Вторая строка таблицы не разделитель |---|: {second_line[:50]}')) # M04: одинаковое количество колонок # Учитываем экранированные трубы \| (не считаем их как разделители) # Учитываем трубы внутри inline-кода `...` (не считаем их как разделители) col_counts: list[tuple[int, int]] = [] for ln, line in block: # Заменяем экранированные \| на placeholder перед подсчётом cleaned = line.replace('\\|', 'PIPE') # Заменяем inline-код `...` на placeholder (трубы внутри не считаем) cleaned = inline_code_re.sub('`CODE`', cleaned) # Считаем количество | минус 1 (т.к. |a|b| = 2 колонки, 3 символа |) count = cleaned.count('|') - 1 if cleaned.startswith('|') and cleaned.endswith('|') else cleaned.count('|') col_counts.append((ln, count)) if len({c for _, c in col_counts}) > 1: # Есть разногласия в количестве колонок expected = col_counts[0][1] for ln, count in col_counts[1:]: if count != expected: issues.append(Issue('M04', ln, 0, f'Колонок: {count}, ожидалось: {expected} ' f'(по заголовку таблицы на строке {start_line})')) return issues def _check_m05_placeholders(split_lines: list[SplitLine]) -> list[Issue]: """M05: угловые плейсхолдеры <...> вне код-блоков (только strict_placeholders).""" issues: list[Issue] = [] placeholder_re = re.compile(r'<([^>]+)>') inline_code_re = re.compile(r'`[^`]+`') # Список допустимых placeholder'ов в руководствах (пользователь заменяет на своё значение) # — это НЕ баг, это инструкция. ALLOWED_PLACEHOLDERS_IN_GUIDE = { 'username', 'owner', 'your_email@example.com', 'you', 'personal-owner', 'work', 'path', 'version', 'X.Y', 'NN', 'N', 'M', 'URL', 'URL_репозитория', } HTML_TAGS_ALLOWED = { 'br', 'sup', 'sub', 'b', '/b', 'i', '/i', 'strong', '/strong', 'em', '/em', 'code', '/code', 'hr', '/hr', } for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue # Пропускаем комментарии HTML if line.strip().startswith('<!--'): continue # Вырезаем inline-код `...` перед проверкой line_no_code = inline_code_re.sub('`CODE`', line) for m in placeholder_re.finditer(line_no_code): content_inner = m.group(1).strip() # Разрешаем: HTML-теги if content_inner.lower() in HTML_TAGS_ALLOWED: continue # Разрешаем: допустимые placeholder'ы в руководствах if content_inner.lower() in ALLOWED_PLACEHOLDERS_IN_GUIDE: continue issues.append(Issue('M05', i + 1, m.start() + 1, f'Незаменённый плейсхолдер: <{content_inner}>. ' f'Все <...> должны быть заменены на реальный контент.')) return issues def _check_m06_english_phrases(split_lines: list[SplitLine], lines: list[str]) -> list[Issue]: """M06: английские разговорные фразы в русской прозе + гибридные слова.""" issues: list[Issue] = [] inline_code_re = re.compile(r'`[^`]+`') for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue # Пропускаем строки таблиц (в них могут быть коды статуса) if line.strip().startswith('|'): continue # Пропускаем строки-цитаты с кодами статуса → OK, → FAIL if '→ OK' in line or '→ FAIL' in line or '→ PASS' in line: continue # Вырезаем inline-код `...` перед проверкой line_without_code = inline_code_re.sub('`CODE`', line) # Пропускаем строки-примеры (содержат кавычки-ёлочки или слово "запрещён/типа/вроде") lower_line = line_without_code.lower() is_example_context = ( '«' in line_without_code or '»' in line_without_code or 'запрещен' in lower_line or 'запрещён' in lower_line or 'типа' in lower_line or 'вроде' in lower_line or 'forbidden' in lower_line or 'пример' in lower_line or 'не используй' in lower_line or 'не использовать' in lower_line or 'english' in lower_line or 'недопустимо' in lower_line or 'корректно' in lower_line ) if is_example_context: continue for pattern in FORBIDDEN_ENGLISH_PHRASES: for m in re.finditer(pattern, line_without_code, re.IGNORECASE): issues.append(Issue('M06', i + 1, m.start() + 1, f'Английская фраза в русской прозе: "{m.group(0)}"')) # Гибридные слова (官方ная и подобные) for i, line in enumerate(lines): for word in FORBIDDEN_HYBRID_WORDS: if word in line: idx = line.index(word) issues.append(Issue('M06', i + 1, idx + 1, f'Гибридное/несуществующее слово: "{word}". ' f'Замените на правильное русское слово.')) return issues def _check_m07_blank_lines_around_headings( split_lines: list[SplitLine], lines: list[str], frontmatter_end: int, ) -> list[Issue]: """M07: пустые строки вокруг заголовков #.""" issues: list[Issue] = [] for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue if not re.match(r'^#{1,6}\s', line): continue # Проверяем строку перед (кроме первой строки файла и frontmatter) if i > 0 and i + 1 > frontmatter_end: prev = lines[i - 1] if prev.strip() and not prev.strip().startswith('```'): issues.append(Issue('M07', i + 1, 0, f'Перед заголовком "{line[:40]}" нет пустой строки ' f'(строка {i}: "{prev[:40]}")')) # Проверяем строку после if i < len(lines) - 1: nxt = lines[i + 1] if nxt.strip() and not nxt.strip().startswith('```'): # Разрешаем YAML-блоки (KEY=value) сразу после заголовка # (например, ## CURRENT_STATE\nCURRENT_STAGE=01) if re.match(r'^[A-Z_]+=', nxt.strip()): continue # Разрешаем таблицы сразу после заголовка if nxt.strip().startswith('|'): continue # Разрешаем ```text/yaml/bash сразу после заголовка if nxt.strip().startswith('```'): continue # Разрешаем нумерованные и маркированные списки сразу после заголовка # (стандартный markdown-стиль: ## Заголовок\n- пункт) if re.match(r'^(\d+\.|-|\*)\s', nxt.strip()): continue issues.append(Issue('M07', i + 1, 0, f'После заголовка "{line[:40]}" нет пустой строки ' f'(строка {i + 2}: "{nxt[:40]}")')) return issues def _check_m14_todo_fixme(split_lines: list[SplitLine]) -> list[Issue]: """M14: TODO/FIXME/XXX/placeholder вне код-блоков (только strict_placeholders).""" issues: list[Issue] = [] for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue for pattern in FORBIDDEN_PLACEHOLDERS: for m in re.finditer(pattern, line, re.IGNORECASE): # Разрешаем упоминание этих слов в контексте правил # (например, "НЕ используй TODO" — это инструкция) if 'НЕ используй' in line or 'запрещен' in line.lower() or 'запрещён' in line.lower(): continue issues.append(Issue('M14', i + 1, m.start() + 1, f'Маркер плейсхолдера: "{m.group(0)}"')) return issues def _check_m15_literal_escapes(split_lines: list[SplitLine]) -> list[Issue]: """M15: literal escape-последовательности \\n, \\t, \\" вне код-блоков. Агент иногда пишет Python-строки с \\n, \\t, \\" напрямую в markdown, и эти escape-последовательности попадают в финальный документ как literal символы. """ issues: list[Issue] = [] LITERAL_ESCAPES = [ (r'\\n', '\\n (literal backslash-n)'), (r'\\t', '\\t (literal backslash-t)'), (r'\\"', '\\" (literal backslash-quote)'), ] for i, (is_in_code, line) in enumerate(split_lines): if is_in_code: continue # Пропустить строки с YAML/JSON значениями, где \" может быть валидным # (например, в frontmatter или в inline YAML) stripped = line.strip() if stripped.startswith('---') or stripped.startswith('...'): continue # Пропустить строки внутри inline code `...` # (вырезаем inline-код перед проверкой) line_no_inline = re.sub(r'`[^`]+`', '`CODE`', line) for pattern, label in LITERAL_ESCAPES: for m in re.finditer(pattern, line_no_inline): # Пропустить если это в кавычках-ёлочках (пример) if '«' in line or '»' in line: continue # Пропустить если это инструкция (упоминание "escape", "literal") lower = line.lower() if 'escape' in lower or 'literal' in lower or 'экраниров' in lower: continue issues.append(Issue('M15', i + 1, m.start() + 1, f'Literal escape-последовательность: {label}. ' f'Python-строка попала в markdown как literal. ' f'Используйте реальный перенос строки или уберите экранирование.')) return issues def _check_phase2_rules( content: str, lines: list[str], split_lines: list[SplitLine], strict_placeholders: bool, is_phase1: bool, is_phase2: bool, ) -> list[Issue]: """M08-M13: правила, применяемые только к Phase 1/Phase 2 docs_target.""" issues: list[Issue] = [] if is_phase1 and not is_phase2: # Phase 1 — только базовые проверки + M08 # M08: маркер Phase 1 присутствует (уже проверено через is_phase1) return issues if not is_phase2: # Не Phase 1 и не Phase 2 — возможно, это статический MD (rules, prompts, README) # В этом случае M08–M13 не применяются return issues # --- M09: маркер Phase 2 присутствует (проверено через is_phase2) --- # --- M10: обязательные секции Phase 2 --- # Применяется только к финальным выводам (docs/), не к rule-файлам, # которые описывают обе фазы как документацию if strict_placeholders: for section in PHASE2_REQUIRED_SECTIONS: if section not in content: issues.append(Issue('M10', 0, 0, f'Отсутствует обязательная секция Phase 2: "{section}"')) # --- M11: маркер Phase 1 должен быть удалён --- # Применяется только к финальным выводам (docs/). Rule-файлы могут # описывать обе фазы (и Phase 1, и Phase 2) как документацию. if strict_placeholders and is_phase1: for i, line in enumerate(lines): if '> Phase 1 — инструкция для AI-агента' in line: issues.append(Issue('M11', i + 1, 0, 'Маркер Phase 1 не удалён. Phase 2 должна полностью ' 'заменить маркер Phase 1.')) break # --- M12: имя секции VERIFIED_CHECKLIST --- if OLD_VERIFIED_NAME in content: for i, line in enumerate(lines): if OLD_VERIFIED_NAME in line: issues.append(Issue('M12', i + 1, 0, f'Старое имя секции: "{line.strip()}". ' f'Должно быть "## VERIFIED_CHECKLIST" (согласовано с ' f'docs-writer.md и step-finalizer.md).')) break # --- M13: все пункты VERIFIED_CHECKLIST закрыты --- in_checklist = False for i, (is_in_code, line) in enumerate(split_lines): if line.strip().startswith('## VERIFIED_CHECKLIST'): in_checklist = True continue if in_checklist: if is_in_code: continue # Если встретили новый ## заголовок — конец VERIFIED_CHECKLIST if line.strip().startswith('## ') and not line.strip().startswith('### '): in_checklist = False continue # Ищем незакрытые пункты [ ] m = re.match(r'^\s*- \[ \]\s+(.+)$', line) if m: issues.append(Issue('M13', i + 1, 0, f'Незакрытый пункт VERIFIED_CHECKLIST: "{m.group(1)[:50]}". ' f'Должен быть [x], не [ ].')) return issues def _find_frontmatter_end(lines: list[str]) -> int: """Найти конец YAML frontmatter (индекс строки после закрывающего ---).""" if not lines or lines[0].strip() != '---': return 0 for i in range(1, len(lines)): if lines[i].strip() == '---': return i + 1 return 0 def validate_file(filepath: Path | str, strict_placeholders: bool = True) -> list[Issue]: """Валидирует один MD-файл. Возвращает список Issue. P1-13 fix: декомпозирована на 8 helper-функций (раньше одна на 388 строк со сложностью F (135)). Каждая helper-функция отвечает за одно правило (M01-M15) и имеет сложность 5-20. Args: filepath: путь к файлу. strict_placeholders: если True — проверять плейсхолдеры <...> (для финальных выводов docs/). Если False — пропустить M05/M14 (для шаблонов .gigacode_vsc/ и корневых файлов, где <placeholder> — часть шаблона). """ issues: list[Issue] = [] filepath = Path(filepath) # Чтение файла — точечный except (P1-15 fix): ловим только OSError. try: content = filepath.read_text(encoding='utf-8') except OSError as e: issues.append(Issue('M00', 0, 0, f'Не удалось прочитать файл: {e}')) return issues lines = content.split('\n') if not lines: issues.append(Issue('M00', 0, 0, 'Файл пустой')) return issues frontmatter_end = _find_frontmatter_end(lines) split_lines = split_code_blocks(lines) # M01: H1 заголовок issues.extend(_check_m01_h1(lines, split_lines, filepath, frontmatter_end)) # M02: балансировка тройных бэктиков issues.extend(_check_m02_backticks(content, split_lines)) # M03, M04: проверка таблиц issues.extend(_check_m03_m04_tables(split_lines)) # M05: угловые плейсхолдеры (только strict_placeholders) if strict_placeholders: issues.extend(_check_m05_placeholders(split_lines)) # M06: английские фразы + гибридные слова issues.extend(_check_m06_english_phrases(split_lines, lines)) # M07: пустые строки вокруг заголовков issues.extend(_check_m07_blank_lines_around_headings(split_lines, lines, frontmatter_end)) # M14: TODO/FIXME/XXX/placeholder (только strict_placeholders) if strict_placeholders: issues.extend(_check_m14_todo_fixme(split_lines)) # M15: literal escape-последовательности issues.extend(_check_m15_literal_escapes(split_lines)) # Phase2 блок (M08-M13) is_phase1 = '> Phase 1 — инструкция для AI-агента' in content is_phase2 = '> Обучающая документация' in content issues.extend(_check_phase2_rules(content, lines, split_lines, strict_placeholders, is_phase1, is_phase2)) return issues def main(): if len(sys.argv) < 2: print(__doc__) print('\nИспользование:') print(' uv run python scripts/validate_md.py <path-to-md-file>') print(' uv run python scripts/validate_md.py --all') sys.exit(2) if sys.argv[1] == '--all': # Проверить все .md в docs/ и .gigacode_vsc/ base = Path(__file__).parent.parent md_files = [] for pattern in ['docs/**/*.md', '.gigacode_vsc/**/*.md', '*.md']: md_files.extend(base.glob(pattern)) md_files = sorted(set(md_files)) if not md_files: print('MD-файлы не найдены.') sys.exit(1) else: md_files = [Path(sys.argv[1])] total_issues = 0 total_errors = 0 total_warnings = 0 print(f"\n{'=' * 70}") print(f"Validate MD — {len(md_files)} файл(ов)") print(f"{'=' * 70}") for filepath in md_files: if not filepath.exists(): print(f'\n📄 {filepath} — НЕ НАЙДЕН') total_errors += 1 continue # Определяем режим проверки: # - Файлы в docs/ (кроме .vitepress/) — финальные выводы, strict_placeholders=True # - Файлы в .gigacode_vsc/ и корневые — шаблоны/правила, strict_placeholders=False path_str = str(filepath) is_final_output = ( '/docs/' in path_str or path_str.endswith('.md') and '/docs' in path_str ) and '.vitepress' not in path_str # Более точная проверка: если путь содержит /docs/ и не содержит /.vitepress/ is_final_output = ('/docs/' in path_str or path_str.startswith('docs/')) and '.vitepress' not in path_str issues = validate_file(filepath, strict_placeholders=is_final_output) errors = [i for i in issues if i.severity == 'ERROR'] warnings = [i for i in issues if i.severity == 'WARN'] total_issues += len(issues) total_errors += len(errors) total_warnings += len(warnings) status = '✅ VALID' if not errors else '❌ INVALID' print(f'\n📄 {filepath} — {status} ' f'({len(errors)} ошибок, {len(warnings)} предупреждений)') if issues: for issue in issues: print(str(issue)) print(f"\n{'=' * 70}") if total_errors: print(f'Итого: {total_errors} ошибок, {total_warnings} предупреждений — INVALID ❌') sys.exit(1) else: if total_warnings: print(f'✅ Все обязательные проверки пройдены — VALID ' f'(с {total_warnings} предупреждениями)') else: print('✅ Все проверки пройдены — VALID') sys.exit(0) if __name__ == '__main__': main()