#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const validationDocs = [ "docs/PILOT_VALIDATION_CHECKLIST_RU.md", "docs/PILOT_GAP_ANALYSIS_RU.md", "docs/CUSTOMER_DISCOVERY_QUESTIONS_RU.md", "docs/PILOT_SUCCESS_CRITERIA_RU.md", "docs/COMPETITIVE_POSITIONING_RU.md", ]; const demoDocs = [ "docs/PILOT_DEMO_SCENARIO_RU.md", "docs/DEMO_RUNBOOK_RU.md", "docs/DEMO_REPORT_EXAMPLE_RU.md", "docs/PILOT_VALUE_PROPOSITION_RU.md", "docs/demo/DEMO_SCENARIO_EXECUTIVE_RU.md", "docs/demo/DEMO_SCENARIO_SECURITY_RU.md", "docs/demo/DEMO_SCENARIO_FORENSICS_RU.md", "docs/demo/DEMO_PACK_ACCEPTANCE_CHECKLIST_RU.md", "docs/fixtures/pilot-v1-demo/README_RU.md", "docs/fixtures/pilot-v1-demo/demo-seed-data.json", ]; const registryDocs = [ "docs/REGISTRY_PRODUCT_PASSPORT_RU.md", "docs/REGISTRY_ARCHITECTURE_RU.md", "docs/REGISTRY_FUNCTIONAL_SCOPE_RU.md", "docs/REGISTRY_DEPENDENCY_STATEMENT_RU.md", "docs/REGISTRY_DEPLOYMENT_MODEL_RU.md", "docs/REGISTRY_COMMERCIAL_POSITIONING_RU.md", "docs/REGISTRY_READINESS_CHECKLIST_RU.md", ]; const deploymentDocs = [ "docs/ENTERPRISE_DEPLOYMENT_GUIDE_RU.md", "docs/DEPLOYMENT_TOPOLOGIES_RU.md", "docs/SIZING_GUIDE_RU.md", "docs/BACKUP_AND_RECOVERY_RU.md", "docs/OPERATIONS_RUNBOOK_RU.md", "docs/SECURITY_HARDENING_RU.md", "docs/ENTERPRISE_ACCEPTANCE_CHECKLIST_RU.md", ]; const screenshots = [ "docs/screenshots/01-executive-overview.png", "docs/screenshots/02-risk-heatmap.png", "docs/screenshots/03-security-view.png", "docs/screenshots/04-operations-view.png", "docs/screenshots/05-investigation-pack.png", "docs/screenshots/06-markdown-report.png", "docs/screenshots/07-product-architecture.png", ]; const roadmapDocs = [ "docs/roadmap/README.md", "docs/roadmap/TASK_001_PILOT_V1_STABILIZATION.md", "docs/roadmap/TASK_002_PRODUCTION_HARDENING.md", "docs/roadmap/TASK_003_EXPLAINABLE_KPI.md", "docs/roadmap/TASK_003A_PORTAL_HARDENING_CLEANUP.md", "docs/roadmap/TASK_004_RISK_NARRATIVE.md", "docs/roadmap/TASK_005_RUST_AGENT_BASELINE.md", "docs/roadmap/TASK_006_EXECUTIVE_ACTION_CENTER.md", "docs/roadmap/TASK_007_CUSTOMER_DEMO_PACK.md", "docs/roadmap/TASK_008_REGISTRY_READINESS.md", "docs/roadmap/TASK_009_ENTERPRISE_DEPLOYMENT_GUIDE.md", "docs/roadmap/TASK_010_PILOT_VALIDATION.md", ]; const reportsAndRunbooks = [ "docs/DEMO_REPORT_EXAMPLE_RU.md", "docs/PRODUCTION_INCIDENT_REPORT_2026-06-07_RU.md", "docs/OPERATIONS_RUNBOOK_RU.md", "docs/OPERATIONS_RUNBOOK_WORKTIME_RU.md", "docs/BACKUP_AND_RECOVERY_RU.md", "docs/DEMO_RUNBOOK_RU.md", ]; function readText(file) { return fs.readFileSync(path.join(root, file), "utf8"); } function exists(file) { return fs.existsSync(path.join(root, file)); } function checkFiles(files) { return files.filter((file) => !exists(file)); } function checkScreenshots(files) { const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const findings = []; for (const file of files) { const fullPath = path.join(root, file); if (!fs.existsSync(fullPath)) { findings.push({ file, reason: "missing" }); continue; } const data = fs.readFileSync(fullPath); if (data.length <= 1000 || !data.subarray(0, 8).equals(pngSignature)) { findings.push({ file, reason: "not_png_or_too_small", size: data.length }); } } return findings; } function checkMarkdownLinks(files) { const findings = []; const linkPattern = /!?\[[^\]]+\]\(([^)]+)\)/g; for (const file of files) { if (!file.endsWith(".md")) continue; let match = null; const text = readText(file); while ((match = linkPattern.exec(text)) !== null) { const raw = String(match[1] || "").trim(); const target = raw.split(/\s+/)[0].replace(/^<|>$/g, ""); if ( !target || target.startsWith("#") || target.startsWith("http://") || target.startsWith("https://") || target.startsWith("mailto:") ) { continue; } const withoutAnchor = target.split("#", 1)[0]; if (!withoutAnchor) continue; const resolved = path.resolve(root, path.dirname(file), withoutAnchor); if (!fs.existsSync(resolved)) { findings.push({ file, target }); } } } return findings; } function checkRequiredContent() { const checks = [ { file: "docs/PILOT_VALIDATION_CHECKLIST_RU.md", markers: ["Executive", "Workforce", "Security", "Forensics", "Agent", "Reporting"], }, { file: "docs/PILOT_GAP_ANALYSIS_RU.md", markers: ["Что готово", "Что требует доработки", "Что не входит в пилот", "Что отложено на roadmap"], }, { file: "docs/CUSTOMER_DISCOVERY_QUESTIONS_RU.md", markers: ["Директор", "Руководитель подразделения", "ИБ", "ИТ", "Эксплуатация"], }, { file: "docs/PILOT_SUCCESS_CRITERIA_RU.md", markers: ["Критерии успеха", "Критерии провала", "KPI пилота", "30 дней"], }, { file: "docs/COMPETITIVE_POSITIONING_RU.md", markers: ["ActivityWatch", "Стахановец", "StaffCop", "SearchInform", "InfoWatch"], }, ]; const findings = []; for (const check of checks) { const text = readText(check.file); for (const marker of check.markers) { if (!text.includes(marker)) { findings.push({ file: check.file, marker }); } } } return findings; } function checkDemoSafety() { const result = spawnSync("bash", ["scripts/check_demo_safety.sh", "--json"], { cwd: root, encoding: "utf8", }); if (result.error) { return { ok: false, error: result.error.message, findings: [], }; } let parsed = null; try { parsed = JSON.parse(result.stdout || "{}"); } catch (error) { return { ok: false, error: `invalid_json: ${error.message}`, stdout: result.stdout, stderr: result.stderr, findings: [], }; } return { ok: result.status === 0 && parsed.ok === true, status: result.status, stderr: result.stderr, findings: parsed.findings || [], scope_files: parsed.scope_files || [], }; } function pass(checks, name, ok, details = {}) { checks.push({ name, ok: Boolean(ok), ...details }); } function main() { const checks = []; const docsToScan = [ "README.md", ...validationDocs, ...demoDocs, ...registryDocs, ...deploymentDocs, ...roadmapDocs, ...reportsAndRunbooks, ]; pass(checks, "validation_docs_exist", checkFiles(validationDocs).length === 0, { missing: checkFiles(validationDocs), }); pass(checks, "demo_docs_exist", checkFiles(demoDocs).length === 0, { missing: checkFiles(demoDocs), }); pass(checks, "registry_docs_exist", checkFiles(registryDocs).length === 0, { missing: checkFiles(registryDocs), }); pass(checks, "deployment_docs_exist", checkFiles(deploymentDocs).length === 0, { missing: checkFiles(deploymentDocs), }); pass(checks, "roadmap_exists", checkFiles(roadmapDocs).length === 0, { missing: checkFiles(roadmapDocs), }); pass(checks, "reports_and_runbooks_exist", checkFiles(reportsAndRunbooks).length === 0, { missing: checkFiles(reportsAndRunbooks), }); const screenshotFindings = checkScreenshots(screenshots); pass(checks, "screenshots_exist_and_are_png", screenshotFindings.length === 0, { findings: screenshotFindings, }); const requiredContentFindings = checkRequiredContent(); pass(checks, "validation_docs_have_required_sections", requiredContentFindings.length === 0, { findings: requiredContentFindings, }); const linkFindings = checkMarkdownLinks(docsToScan); pass(checks, "markdown_links_valid", linkFindings.length === 0, { findings: linkFindings, }); const demoSafety = checkDemoSafety(); pass(checks, "demo_safety_scan", demoSafety.ok, { findings: demoSafety.findings, scope_files: demoSafety.scope_files, status: demoSafety.status, stderr: demoSafety.stderr, error: demoSafety.error, }); const ok = checks.every((check) => check.ok); process.stdout.write(`${JSON.stringify({ ok, generated_at_utc: new Date().toISOString(), checks, }, null, 2)}\n`); process.exitCode = ok ? 0 : 2; } main();