Files
contracts-flask/site/templates/architecture-diagram.html
naeel c1e5df2df5
Deploy contracts-flask / validate (push) Successful in 0s
feat: architecture block diagrams page
2026-06-29 12:30:15 +04:00

301 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Архитектура — блок-схемы</title>
<style>
:root {
--bg: #0d1117; --fg: #c9d1d9; --muted: #8b949e;
--accent: #58a6ff; --green: #3fb950; --border: #30363d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font: 15px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 960px; margin: 0 auto; padding: 32px 20px 80px; }
h1 { font-size: 26px; margin-bottom: 8px; }
h2 { font-size: 18px; margin-top: 48px; margin-bottom: 16px; color: var(--accent); }
.nav { margin-bottom: 24px; }
.nav a { color: var(--accent); text-decoration: none; }
.nav a:hover { text-decoration: underline; }
svg { display: block; max-width: 100%; height: auto; }
</style>
</head>
<body>
<div class="nav"><a href="/docs/architect">← Архитектура (текст)</a></div>
<h1>🏗️ Архитектура — блок-схемы</h1>
<h2>1. Система — компоненты и связи</h2>
<svg viewBox="0 0 920 340" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="a1" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto"><path d="M0,0 L7,2.5 L0,5 Z" fill="#58a6ff"/></marker>
<marker id="a2" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto"><path d="M0,0 L7,2.5 L0,5 Z" fill="#3fb950"/></marker>
<filter id="sh"><feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.25"/></filter>
<linearGradient id="gradFlask" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a2a3a"/><stop offset="100%" stop-color="#0d1a28"/></linearGradient>
<linearGradient id="gradVM" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a2a1a"/><stop offset="100%" stop-color="#0d1a0d"/></linearGradient>
<linearGradient id="gradDB" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#2a1a2a"/><stop offset="100%" stop-color="#1a0d1a"/></linearGradient>
<linearGradient id="gradLLM" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3a2a1a"/><stop offset="100%" stop-color="#281a0d"/></linearGradient>
<linearGradient id="gradUser" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#2a1a1a"/><stop offset="100%" stop-color="#1a0d0d"/></linearGradient>
</defs>
<!-- User -->
<rect x="30" y="100" width="140" height="90" rx="10" fill="url(#gradUser)" stroke="#f85149" stroke-width="1.5" filter="url(#sh)"/>
<text x="100" y="135" text-anchor="middle" fill="#f85149" font-size="15" font-weight="bold">👤 Пользователь</text>
<text x="100" y="155" text-anchor="middle" fill="#8b949e" font-size="11">Браузер</text>
<text x="100" y="172" text-anchor="middle" fill="#8b949e" font-size="11">Загрузка файлов</text>
<!-- Arrow User → Flask -->
<line x1="172" y1="130" x2="230" y2="80" stroke="#f85149" stroke-width="1.5" marker-end="url(#a1)"/>
<text x="205" y="95" fill="#8b949e" font-size="9">HTTPS</text>
<line x1="172" y1="150" x2="230" y2="150" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#a1)"/>
<text x="205" y="143" fill="#8b949e" font-size="9">SSE</text>
<!-- Flask -->
<rect x="235" y="50" width="160" height="120" rx="10" fill="url(#gradFlask)" stroke="#58a6ff" stroke-width="1.5" filter="url(#sh)"/>
<text x="315" y="80" text-anchor="middle" fill="#58a6ff" font-size="14" font-weight="bold">⚡ Flask-фронтенд</text>
<text x="315" y="100" text-anchor="middle" fill="#8b949e" font-size="10">Managed Python (Nubes)</text>
<text x="315" y="118" text-anchor="middle" fill="#8b949e" font-size="10">Jinja2-шаблоны</text>
<text x="315" y="136" text-anchor="middle" fill="#8b949e" font-size="10">Промпт-редактор</text>
<text x="315" y="154" text-anchor="middle" fill="#8b949e" font-size="10">Чат с LLM</text>
<!-- Arrow Flask → VM -->
<line x1="397" y1="110" x2="455" y2="110" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#a1)"/>
<text x="430" y="103" fill="#8b949e" font-size="9">REST API</text>
<!-- VM box -->
<rect x="460" y="15" width="230" height="210" rx="10" fill="none" stroke="#30363d" stroke-width="1" stroke-dasharray="6,4"/>
<text x="575" y="35" text-anchor="middle" fill="#8b949e" font-size="11">Выделенная VM</text>
<!-- Parsing -->
<rect x="480" y="50" width="190" height="50" rx="8" fill="url(#gradVM)" stroke="#3fb950" stroke-width="1" filter="url(#sh)"/>
<text x="575" y="72" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📄 Парсинг документов</text>
<text x="575" y="90" text-anchor="middle" fill="#8b949e" font-size="10">pdfplumber · python-docx</text>
<!-- Classification -->
<rect x="480" y="110" width="190" height="50" rx="8" fill="url(#gradVM)" stroke="#bc8cff" stroke-width="1" filter="url(#sh)"/>
<text x="575" y="132" text-anchor="middle" fill="#bc8cff" font-size="12" font-weight="bold">🏷️ Классификация</text>
<text x="575" y="150" text-anchor="middle" fill="#8b949e" font-size="10">3-этапный фильтр мусора</text>
<!-- Comparison -->
<rect x="480" y="170" width="190" height="45" rx="8" fill="url(#gradVM)" stroke="#d2991d" stroke-width="1" filter="url(#sh)"/>
<text x="575" y="190" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">🤖 LLM-сравнение</text>
<text x="575" y="207" text-anchor="middle" fill="#8b949e" font-size="10">extract + diff · группировка</text>
<!-- Internal arrows -->
<line x1="575" y1="100" x2="575" y2="108" stroke="#58a6ff" stroke-width="1" marker-end="url(#a1)"/>
<line x1="575" y1="160" x2="575" y2="168" stroke="#58a6ff" stroke-width="1" marker-end="url(#a1)"/>
<!-- Arrow VM → LLM -->
<line x1="692" y1="140" x2="750" y2="90" stroke="#d2991d" stroke-width="1.5" marker-end="url(#a1)"/>
<text x="725" y="108" fill="#8b949e" font-size="9">HTTP/2</text>
<!-- LLM -->
<rect x="755" y="60" width="140" height="80" rx="10" fill="url(#gradLLM)" stroke="#d2991d" stroke-width="1.5" filter="url(#sh)"/>
<text x="825" y="90" text-anchor="middle" fill="#d2991d" font-size="13" font-weight="bold">🧠 LLM</text>
<text x="825" y="110" text-anchor="middle" fill="#8b949e" font-size="10">api.aillm.ru</text>
<text x="825" y="128" text-anchor="middle" fill="#8b949e" font-size="10">gpt-oss-120b</text>
<!-- Arrow VM ↔ DB -->
<line x1="575" y1="228" x2="575" y2="260" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#a1)"/>
<!-- DB -->
<rect x="470" y="265" width="210" height="55" rx="10" fill="url(#gradDB)" stroke="#bc8cff" stroke-width="1.5" filter="url(#sh)"/>
<text x="575" y="290" text-anchor="middle" fill="#bc8cff" font-size="13" font-weight="bold">🗄️ PostgreSQL 15</text>
<text x="575" y="310" text-anchor="middle" fill="#8b949e" font-size="10">JSONB · Event Sourcing · spec_events</text>
</svg>
<h2>2. Пайплайн обработки одного файла</h2>
<svg viewBox="0 0 920 280" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="b1" markerWidth="6" markerHeight="4" refX="6" refY="2" orient="auto"><path d="M0,0 L6,2 L0,4 Z" fill="#58a6ff"/></marker>
<marker id="b2" markerWidth="6" markerHeight="4" refX="6" refY="2" orient="auto"><path d="M0,0 L6,2 L0,4 Z" fill="#f85149"/></marker>
<filter id="sh2"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.2"/></filter>
</defs>
<!-- Stage 1 -->
<rect x="10" y="30" width="135" height="90" rx="8" fill="#1a2a1a" stroke="#3fb950" stroke-width="1.5" filter="url(#sh2)"/>
<text x="77" y="55" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📤 Загрузка</text>
<text x="77" y="73" text-anchor="middle" fill="#8b949e" font-size="9">Multipart-парсинг</text>
<text x="77" y="88" text-anchor="middle" fill="#8b949e" font-size="9">SHA-256 хеш</text>
<text x="77" y="103" text-anchor="middle" fill="#8b949e" font-size="9">Формат: pdf/docx/zip</text>
<line x1="147" y1="75" x2="172" y2="75" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#b1)"/>
<!-- Stage 2 -->
<rect x="177" y="30" width="135" height="90" rx="8" fill="#1a2a3a" stroke="#58a6ff" stroke-width="1.5" filter="url(#sh2)"/>
<text x="244" y="55" text-anchor="middle" fill="#58a6ff" font-size="12" font-weight="bold">🔍 Парсинг</text>
<text x="244" y="73" text-anchor="middle" fill="#8b949e" font-size="9">Извлечение текста</text>
<text x="244" y="88" text-anchor="middle" fill="#8b949e" font-size="9">Извлечение таблиц</text>
<text x="244" y="103" text-anchor="middle" fill="#8b949e" font-size="9">→ elements_json</text>
<line x1="314" y1="75" x2="339" y2="75" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#b1)"/>
<!-- Stage 3a -->
<rect x="344" y="30" width="145" height="90" rx="8" fill="#2a1a3a" stroke="#bc8cff" stroke-width="1.5" filter="url(#sh2)"/>
<text x="416" y="52" text-anchor="middle" fill="#bc8cff" font-size="12" font-weight="bold">🏷️ Фильтр #1-#2</text>
<text x="416" y="68" text-anchor="middle" fill="#8b949e" font-size="9">Имя файла → мусор?</text>
<text x="416" y="83" text-anchor="middle" fill="#8b949e" font-size="9">Заголовки → мусор?</text>
<text x="416" y="98" text-anchor="middle" fill="#8b949e" font-size="9">0 токенов LLM</text>
<text x="416" y="113" text-anchor="middle" fill="#f85149" font-size="8">мусор → стоп</text>
<line x1="491" y1="75" x2="516" y2="75" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#b1)"/>
<!-- Stage 3b -->
<rect x="521" y="30" width="145" height="90" rx="8" fill="#2a2a1a" stroke="#d2991d" stroke-width="1.5" filter="url(#sh2)"/>
<text x="593" y="52" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">🤖 Фильтр #3 — LLM</text>
<text x="593" y="68" text-anchor="middle" fill="#8b949e" font-size="9">Тип: договор/допник/</text>
<text x="593" y="83" text-anchor="middle" fill="#8b949e" font-size="9">спецификация/мусор</text>
<text x="593" y="98" text-anchor="middle" fill="#8b949e" font-size="9">Номер, дата, контрагент</text>
<text x="593" y="113" text-anchor="middle" fill="#f85149" font-size="8">мусор → стоп</text>
<line x1="668" y1="75" x2="693" y2="75" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#b1)"/>
<!-- Stage 4 -->
<rect x="698" y="30" width="100" height="90" rx="8" fill="#3a2a1a" stroke="#d2991d" stroke-width="1.5" filter="url(#sh2)"/>
<text x="748" y="60" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">📋 Группи-</text>
<text x="748" y="78" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">ровка</text>
<text x="748" y="96" text-anchor="middle" fill="#8b949e" font-size="9">По номерам</text>
<text x="748" y="111" text-anchor="middle" fill="#8b949e" font-size="9">договоров</text>
<line x1="800" y1="75" x2="825" y2="75" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#b1)"/>
<!-- Stage 5 -->
<rect x="830" y="30" width="80" height="90" rx="8" fill="#1a3a2a" stroke="#3fb950" stroke-width="1.5" filter="url(#sh2)"/>
<text x="870" y="60" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">Срав-</text>
<text x="870" y="78" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">нение</text>
<text x="870" y="100" text-anchor="middle" fill="#8b949e" font-size="9">ADD</text>
<text x="870" y="114" text-anchor="middle" fill="#8b949e" font-size="9">UPDATE</text>
<!-- Protection Bar -->
<rect x="10" y="145" width="900" height="40" rx="6" fill="#1a0a0a" stroke="#f85149" stroke-width="1" stroke-dasharray="3,2"/>
<text x="460" y="170" text-anchor="middle" fill="#f85149" font-size="11" font-weight="bold">🛡️ Защита: ZIP-бомба (500/500/100×) · Path traversal · Whitelist форматов · content_hash · UNRESOLVED · нормализация чисел/дат · price×qty=sum</text>
<!-- Output Bar -->
<rect x="10" y="210" width="900" height="50" rx="6" fill="#161b22" stroke="#3fb950" stroke-width="1.5" filter="url(#sh2)"/>
<text x="460" y="233" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📊 База: documents (метаданные + elements_json) · classify_results · spec_events (аудит) · spec_current (спецификация) · prompts (версионирование)</text>
<text x="460" y="250" text-anchor="middle" fill="#8b949e" font-size="9">Бинарные файлы удаляются после парсинга. Промежуточные данные — требуется автоочистка.</text>
</svg>
<h2>3. Event Sourcing — как работает сравнение</h2>
<svg viewBox="0 0 920 380" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="c1" markerWidth="6" markerHeight="4" refX="6" refY="2" orient="auto"><path d="M0,0 L6,2 L0,4 Z" fill="#58a6ff"/></marker>
<filter id="sh3"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.2"/></filter>
</defs>
<!-- Documents row -->
<text x="460" y="24" text-anchor="middle" fill="#8b949e" font-size="12">Документы группы (отсортированы по doc_date)</text>
<rect x="10" y="38" width="100" height="55" rx="6" fill="#1a2a3a" stroke="#58a6ff" stroke-width="1" filter="url(#sh3)"/>
<text x="60" y="60" text-anchor="middle" fill="#58a6ff" font-size="10" font-weight="bold">📄 Договор</text>
<text x="60" y="78" text-anchor="middle" fill="#8b949e" font-size="9">базовый</text>
<rect x="130" y="38" width="80" height="55" rx="6" fill="#1a2a3a" stroke="#58a6ff" stroke-width="1" filter="url(#sh3)"/>
<text x="170" y="60" text-anchor="middle" fill="#58a6ff" font-size="10" font-weight="bold">📄 Допник</text>
<text x="170" y="78" text-anchor="middle" fill="#8b949e" font-size="9">#1</text>
<rect x="230" y="38" width="80" height="55" rx="6" fill="#1a2a3a" stroke="#58a6ff" stroke-width="1" filter="url(#sh3)"/>
<text x="270" y="60" text-anchor="middle" fill="#58a6ff" font-size="10" font-weight="bold">📄 Допник</text>
<text x="270" y="78" text-anchor="middle" fill="#8b949e" font-size="9">#2</text>
<text x="350" y="70" fill="#8b949e" font-size="20">...</text>
<!-- Arrow to Extract -->
<line x1="60" y1="95" x2="60" y2="130" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Extract box -->
<rect x="10" y="135" width="200" height="70" rx="8" fill="#1a2a3a" stroke="#d2991d" stroke-width="1.5" filter="url(#sh3)"/>
<text x="110" y="158" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">🤖 LLM: промпт extract</text>
<text x="110" y="176" text-anchor="middle" fill="#8b949e" font-size="10">Извлечение всех услуг</text>
<text x="110" y="194" text-anchor="middle" fill="#8b949e" font-size="10">→ массив ADD-операций</text>
<line x1="212" y1="170" x2="242" y2="170" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Events box -->
<rect x="247" y="130" width="200" height="80" rx="8" fill="#2a1a2a" stroke="#bc8cff" stroke-width="1.5" filter="url(#sh3)"/>
<text x="347" y="153" text-anchor="middle" fill="#bc8cff" font-size="12" font-weight="bold">🗄️ spec_events</text>
<text x="347" y="171" text-anchor="middle" fill="#8b949e" font-size="10">ADD услуга X</text>
<text x="347" y="187" text-anchor="middle" fill="#8b949e" font-size="10">ADD услуга Y</text>
<text x="347" y="203" text-anchor="middle" fill="#8b949e" font-size="10">(неизменяемый лог)</text>
<line x1="347" y1="212" x2="347" y2="242" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Current spec -->
<rect x="247" y="247" width="200" height="70" rx="8" fill="#1a2a1a" stroke="#3fb950" stroke-width="1.5" filter="url(#sh3)"/>
<text x="347" y="272" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📊 spec_current</text>
<text x="347" y="292" text-anchor="middle" fill="#8b949e" font-size="10">Услуга X | 100₽ | 10 шт</text>
<text x="347" y="308" text-anchor="middle" fill="#8b949e" font-size="10">Услуга Y | 200₽ | 5 шт</text>
<!-- Arrow back to LLM for diff -->
<line x1="449" y1="282" x2="500" y2="282" stroke="#3fb950" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Diff box -->
<rect x="505" y="135" width="200" height="70" rx="8" fill="#3a2a1a" stroke="#d2991d" stroke-width="1.5" filter="url(#sh3)"/>
<text x="605" y="158" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">🤖 LLM: промпт diff</text>
<text x="605" y="176" text-anchor="middle" fill="#8b949e" font-size="10">(допник #1) vs spec_current</text>
<text x="605" y="194" text-anchor="middle" fill="#8b949e" font-size="10">→ ADD / UPDATE / DELETE</text>
<line x1="707" y1="170" x2="737" y2="170" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Events 2 -->
<rect x="742" y="130" width="168" height="80" rx="8" fill="#2a1a2a" stroke="#bc8cff" stroke-width="1.5" filter="url(#sh3)"/>
<text x="826" y="153" text-anchor="middle" fill="#bc8cff" font-size="12" font-weight="bold">🗄️ spec_events</text>
<text x="826" y="171" text-anchor="middle" fill="#8b949e" font-size="10">UPDATE услуга X</text>
<text x="826" y="187" text-anchor="middle" fill="#8b949e" font-size="10">DELETE услуга Y</text>
<text x="826" y="203" text-anchor="middle" fill="#8b949e" font-size="10">ADD услуга Z</text>
<line x1="826" y1="212" x2="826" y2="242" stroke="#58a6ff" stroke-width="1.5" marker-end="url(#c1)"/>
<!-- Updated spec -->
<rect x="726" y="247" width="184" height="70" rx="8" fill="#1a2a1a" stroke="#3fb950" stroke-width="1.5" filter="url(#sh3)"/>
<text x="818" y="272" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">📊 spec_current (обн.)</text>
<text x="818" y="292" text-anchor="middle" fill="#8b949e" font-size="10">Услуга X | 150₽ | 10 шт</text>
<text x="818" y="308" text-anchor="middle" fill="#8b949e" font-size="10">Услуга Z | 300₽ | 2 шт</text>
<!-- Arrows between events -->
<line x1="473" y1="170" x2="505" y2="150" stroke="#d2991d" stroke-width="1" stroke-dasharray="3,2"/>
<text x="492" y="148" fill="#8b949e" font-size="8">цикл по допникам</text>
<!-- Legend -->
<rect x="10" y="340" width="900" height="30" rx="4" fill="#161b22" stroke="#30363d" stroke-width="0.5"/>
<text x="460" y="360" text-anchor="middle" fill="#8b949e" font-size="10">Каждый допник сравнивается с актуальной spec_current → операции накапливаются в spec_events → spec_current пересчитывается</text>
</svg>
<h2>4. Защита — эшелоны</h2>
<svg viewBox="0 0 920 220" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="d1" markerWidth="5" markerHeight="3" refX="5" refY="1.5" orient="auto"><path d="M0,0 L5,1.5 L0,3 Z" fill="#f85149"/></marker>
<filter id="sh4"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.2"/></filter>
</defs>
<!-- Layer 1: Upload -->
<rect x="10" y="10" width="900" height="40" rx="6" fill="#1a0a0a" stroke="#f85149" stroke-width="1" filter="url(#sh4)"/>
<text x="460" y="35" text-anchor="middle" fill="#f85149" font-size="12" font-weight="bold">🔴 Уровень 1 — Загрузка: ZIP-бомба (500 файлов / 500MB / ratio 100×) · Path traversal · Whitelist .pdf .docx .doc .zip</text>
<line x1="460" y1="50" x2="460" y2="58" stroke="#f85149" stroke-width="1" marker-end="url(#d1)"/>
<!-- Layer 2: Parse -->
<rect x="30" y="62" width="860" height="40" rx="6" fill="#2a1a0a" stroke="#d2991d" stroke-width="1" filter="url(#sh4)"/>
<text x="460" y="87" text-anchor="middle" fill="#d2991d" font-size="12" font-weight="bold">🟠 Уровень 2 — Парсинг: cp437→utf8 для битых имён · content_hash (SHA-256) · дедупликация · try/except → logging.warning</text>
<line x1="460" y1="102" x2="460" y2="110" stroke="#f85149" stroke-width="1" marker-end="url(#d1)"/>
<!-- Layer 3: Classify -->
<rect x="50" y="114" width="820" height="40" rx="6" fill="#2a2a0a" stroke="#e3b341" stroke-width="1" filter="url(#sh4)"/>
<text x="460" y="139" text-anchor="middle" fill="#e3b341" font-size="12" font-weight="bold">🟡 Уровень 3 — Классификация: фильтр по имени файла · фильтр по заголовкам (2000 симв.) · LLM-фильтр · мусор → garbage</text>
<line x1="460" y1="154" x2="460" y2="162" stroke="#f85149" stroke-width="1" marker-end="url(#d1)"/>
<!-- Layer 4: Compare -->
<rect x="70" y="166" width="780" height="40" rx="6" fill="#1a2a0a" stroke="#3fb950" stroke-width="1" filter="url(#sh4)"/>
<text x="460" y="191" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="bold">🟢 Уровень 4 — Сравнение: ADD без имени → UNRESOLVED · UPDATE/DELETE без цели → UNRESOLVED · нормализация 1 000,50→1000.50 · дат · price×qty=sum</text>
</svg>
</body>
</html>