Files

193 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate source data for the Everest knowledge base."""
from __future__ import annotations
from collections import Counter
from kb_common import (
ANALOG_WARNING,
BRAND_WARNING,
FORBIDDEN_BRAND_PHRASES,
OPERATION_WARNING,
REPORTS_DIR,
VALID_STATUSES,
load_articles,
load_sections,
)
REQUIRED_BODY_FIELDS = {
"purpose",
"description",
"key_concepts",
"verification_rules",
"common_errors",
"application_example",
}
EXPECTED_COUNTS = {"1": 17, "2": 14, "3": 15, "4": 22, "5": 20, "6": 20, "7": 20, "8": 20}
def add(errors: list[str], message: str) -> None:
errors.append(f"ERROR: {message}")
def add_warning(warnings: list[str], message: str) -> None:
warnings.append(f"WARNING: {message}")
def validate() -> tuple[list[str], list[str]]:
sections = load_sections()
articles = load_articles()
errors: list[str] = []
warnings: list[str] = []
if len(sections) != 8:
add(errors, f"Expected 8 sections, found {len(sections)}")
section_numbers = [section["number"] for section in sections]
if section_numbers != [str(number) for number in range(1, 9)]:
add(errors, f"Section numbering mismatch: {section_numbers}")
article_numbers = [article["number"] for article in articles]
for number, count in Counter(article_numbers).items():
if count > 1:
add(errors, f"Duplicate article number: {number}")
titles_by_section = Counter((article["section_number"], article["title"].casefold()) for article in articles)
for (section_number, title), count in titles_by_section.items():
if count > 1:
add(errors, f"Duplicate article title in section {section_number}: {title}")
articles_by_number = {article["number"]: article for article in articles}
for section in sections:
expected_count = EXPECTED_COUNTS.get(section["number"])
if len(section["articles"]) != expected_count:
add(
errors,
f"Section {section['number']} expected {expected_count} articles, "
f"found {len(section['articles'])}",
)
expected_numbers = [f"{section['number']}.{idx}" for idx in range(1, len(section["articles"]) + 1)]
if section["articles"] != expected_numbers:
add(errors, f"Section {section['number']} article numbering mismatch")
for article_number in section["articles"]:
if article_number not in articles_by_number:
add(errors, f"Section {section['number']} references missing article {article_number}")
for article in articles:
prefix = f"Article {article.get('number', '<unknown>')}"
for field in (
"number",
"title",
"section_number",
"section_title",
"order",
"body",
"related_articles",
"warnings",
"data_statuses",
):
if field not in article:
add(errors, f"{prefix} missing required field: {field}")
body = article.get("body", {})
for field in REQUIRED_BODY_FIELDS:
if not body.get(field):
add(errors, f"{prefix} missing body field: {field}")
if len(body.get("key_concepts", [])) < 3:
add(errors, f"{prefix} must contain at least 3 key concepts")
if len(body.get("verification_rules", [])) < 3:
add(errors, f"{prefix} must contain at least 3 verification rules")
if len(body.get("common_errors", [])) < 3:
add(errors, f"{prefix} must contain at least 3 common errors")
statuses = set(article.get("data_statuses", []))
if not statuses:
add(errors, f"{prefix} has no data statuses")
if not statuses.issubset(VALID_STATUSES):
add(errors, f"{prefix} has invalid data statuses: {sorted(statuses - VALID_STATUSES)}")
if not statuses.intersection(VALID_STATUSES):
add(errors, f"{prefix} has no allowed uncertainty status")
title_and_section = f"{article.get('title', '')} {article.get('section_title', '')}".lower()
if "аналог" in title_and_section and ANALOG_WARNING not in article.get("warnings", []):
add(errors, f"{prefix} analog warning is missing")
if (
article.get("section_title") == "Эксплуатация, монтаж и диагностика"
and OPERATION_WARNING not in article.get("warnings", [])
):
add(errors, f"{prefix} operation warning is missing")
if article.get("section_title") == "Бренды и производители":
if BRAND_WARNING not in article.get("warnings", []):
add(errors, f"{prefix} brand restrictions warning is missing")
searchable = " ".join(
[
article.get("title", ""),
body.get("description", ""),
body.get("purpose", ""),
body.get("application_example", ""),
]
).casefold()
for phrase in FORBIDDEN_BRAND_PHRASES:
if phrase in searchable:
add(errors, f"{prefix} contains forbidden brand phrase: {phrase}")
if "table" in body:
table = body["table"]
if table.get("columns") != ["Параметр", "Значение", "Источник", "Статус"]:
add(errors, f"{prefix} table columns mismatch")
if not table.get("rows"):
add(errors, f"{prefix} table has no rows")
if "латинский маркер незавершенности" in str(article).lower() or "технический маркер" in str(article).lower():
add(errors, f"{prefix} contains a technical marker")
source_article_numbers = set(article_numbers)
for article in articles:
related_articles = article.get("related_articles", [])
if len(related_articles) != 3:
add(errors, f"Article {article['number']} must contain exactly 3 related articles")
if len(related_articles) != len(set(related_articles)):
add(errors, f"Article {article['number']} contains duplicate related articles")
for related in related_articles:
if related not in source_article_numbers:
add_warning(warnings, f"Article {article['number']} references missing related article {related}")
return errors, warnings
def write_report(errors: list[str], warnings: list[str]) -> None:
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
sections = load_sections()
articles = load_articles()
lines = [
"# Validation report — Технический справочник «Эверест»",
"",
f"- Разделов: {len(sections)}",
f"- Статей: {len(articles)}",
f"- Критических ошибок: {len(errors)}",
f"- Предупреждений: {len(warnings)}",
"",
"## Результат",
"Проверка пройдена." if not errors else "Проверка завершилась с критическими ошибками.",
"",
"## Ошибки",
]
lines.extend(f"- {error}" for error in errors) if errors else lines.append("- нет")
lines.append("")
lines.append("## Предупреждения")
lines.extend(f"- {warning}" for warning in warnings) if warnings else lines.append("- нет")
lines.append("")
(REPORTS_DIR / "validation-report.md").write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
errors, warnings = validate()
write_report(errors, warnings)
for message in errors + warnings:
print(message)
if errors:
print("Validation failed. See reports/validation-report.md")
return 1
print("Validation passed. See reports/validation-report.md")
return 0
if __name__ == "__main__":
raise SystemExit(main())