Merge branch 'docs/regulation-html' into 'main'

docs: HTML-вариант регламента (standalone, brand-styled)

See merge request 2-6/2-6-4/terravault/ordinis!249
This commit is contained in:
Александр Зимин
2026-05-20 15:52:47 +00:00
+588
View File
@@ -0,0 +1,588 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Регламент · Что заводить в Ordinis НСИ</title>
<style>
/* === Warm Terracotta palette — brand-consistent с PPTX === */
:root {
--ink: #1a1714;
--ink-2: #52483d;
--mute: #8c8276;
--accent: #b05a2e;
--accent-bg: #f4d9cd;
--navy: #3a2818;
--bg: #faf9f5;
--surface: #ffffff;
--surface-2: #f5f3ec;
--green: #4f7038;
--green-bg: #e4ead0;
--warn: #a8762a;
--warn-bg: #f6e8bc;
--pink: #a64636;
--pink-bg: #f4d9cd;
--line: #e6e1d4;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 880px;
margin: 0 auto;
padding: 40px 32px 80px;
}
/* === Header / hero === */
.hero {
background: var(--navy);
color: #fff;
margin: -40px -32px 32px;
padding: 48px 32px 40px;
border-left: 6px solid var(--accent);
position: relative;
}
.hero .eyebrow {
color: var(--accent-bg);
font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
margin: 0 0 12px;
}
.hero h1 {
font: 700 34px/1.15 Georgia, "Times New Roman", serif;
margin: 0 0 12px;
color: #fff;
}
.hero .lead {
color: #cadcfc;
font-size: 15px;
line-height: 1.6;
max-width: 680px;
margin: 0;
}
.hero .tldr {
margin-top: 24px;
padding: 14px 18px;
background: rgba(176, 90, 46, 0.18);
border-left: 3px solid var(--accent);
border-radius: 4px;
font-size: 14px;
color: #fff;
}
.hero .tldr strong { color: var(--accent-bg); }
/* === Section headings === */
h2 {
font: 700 24px/1.25 Georgia, "Times New Roman", serif;
color: var(--ink);
margin: 56px 0 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
gap: 12px;
}
h2 .dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
h3 {
font: 600 18px/1.3 Georgia, "Times New Roman", serif;
color: var(--ink);
margin: 32px 0 12px;
}
h4 {
font: 600 15px/1.3 -apple-system, sans-serif;
color: var(--ink-2);
margin: 20px 0 8px;
}
p { margin: 0 0 12px; }
p.muted { color: var(--ink-2); font-size: 14px; }
ul, ol { margin: 0 0 16px; padding-left: 24px; }
li { margin: 4px 0; }
/* === Criterion cards (positive — green accent) === */
.criterion {
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--green);
border-radius: 6px;
padding: 18px 20px;
margin: 16px 0;
}
.criterion h3 {
margin: 0 0 8px;
color: var(--ink);
}
.criterion h3::before {
content: "✓";
color: var(--green);
font-weight: 700;
margin-right: 8px;
}
.criterion .hint {
margin-top: 10px;
padding: 8px 12px;
background: var(--green-bg);
border-radius: 4px;
font-size: 13px;
color: var(--ink-2);
font-style: italic;
}
.criterion .hint::before { content: "💡 "; font-style: normal; }
/* === Anti-pattern cards (negative — pink accent) === */
.anti {
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--pink);
border-radius: 6px;
padding: 16px 20px;
margin: 12px 0;
}
.anti h3 {
margin: 0 0 6px;
color: var(--pink);
font-size: 16px;
}
.anti h3::before {
content: "✗";
margin-right: 8px;
}
.anti .symptom {
margin-top: 8px;
font-size: 13px;
color: var(--ink-2);
font-style: italic;
}
.anti .symptom::before { content: "Признак: "; font-style: normal; font-weight: 600; }
/* === Tables === */
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0 20px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
}
th, td {
text-align: left;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
font-size: 14px;
}
th {
background: var(--surface-2);
font-weight: 600;
color: var(--ink-2);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.08em;
}
tr:last-child td { border-bottom: none; }
td.yes { color: var(--green); font-weight: 600; }
td.no { color: var(--pink); font-weight: 600; }
/* === Checklist === */
.checklist {
background: var(--accent-bg);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 24px;
margin: 24px 0;
}
.checklist h3 {
margin: 0 0 16px;
color: var(--accent);
}
.checklist h3::before {
content: "📋 ";
}
.checklist ol {
counter-reset: cl-item;
list-style: none;
padding-left: 0;
margin: 0;
}
.checklist li {
counter-increment: cl-item;
padding: 8px 0 8px 36px;
position: relative;
border-bottom: 1px dashed rgba(176, 90, 46, 0.3);
font-size: 14px;
}
.checklist li:last-child { border-bottom: none; }
.checklist li::before {
content: counter(cl-item);
position: absolute;
left: 0;
top: 8px;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--accent);
color: #fff;
font-weight: 700;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
font-family: ui-monospace, monospace;
}
.checklist .verdict {
margin-top: 16px;
padding: 12px 16px;
background: rgba(58, 40, 24, 0.06);
border-left: 3px solid var(--navy);
font-size: 13px;
color: var(--ink-2);
}
.checklist .verdict strong { color: var(--ink); }
/* === Process steps === */
.process {
counter-reset: step;
margin: 16px 0 24px;
}
.process > div {
counter-increment: step;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 14px 18px 14px 56px;
margin-bottom: 10px;
position: relative;
font-size: 14px;
}
.process > div::before {
content: counter(step);
position: absolute;
left: 12px;
top: 12px;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--accent);
color: #fff;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
font-family: Georgia, serif;
}
.process strong { color: var(--ink); }
/* === Code / monospaced === */
code {
font: 13px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
background: var(--surface-2);
color: var(--accent);
padding: 1px 6px;
border-radius: 3px;
border: 1px solid var(--line);
}
pre {
background: var(--navy);
color: #fff;
padding: 14px 16px;
border-radius: 6px;
overflow-x: auto;
font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
margin: 12px 0;
}
pre code {
background: none;
color: inherit;
border: none;
padding: 0;
}
/* === Strong inline === */
strong { color: var(--ink); font-weight: 600; }
em { color: var(--ink-2); font-style: italic; }
/* === Convention list (technical bullets) === */
.conventions li {
padding: 6px 0;
font-size: 14px;
}
.conventions code { font-size: 12.5px; }
/* === Anti-pattern callouts (final section) === */
.callouts > div {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 4px;
padding: 12px 16px;
margin: 8px 0;
display: flex;
gap: 12px;
align-items: flex-start;
}
.callouts > div::before {
content: "❌";
flex-shrink: 0;
font-size: 16px;
margin-top: 1px;
}
.callouts > div p { margin: 0; font-size: 14px; }
/* === Footer === */
.foot {
margin-top: 60px;
padding: 24px;
background: var(--surface-2);
border: 1px solid var(--line);
border-left: 4px solid var(--accent);
border-radius: 6px;
font-size: 14px;
}
.foot strong { display: block; margin-bottom: 8px; color: var(--ink); font-size: 15px; }
.foot ul { margin: 0; padding-left: 20px; color: var(--ink-2); }
.foot li { margin: 4px 0; }
.meta {
margin-top: 40px;
padding-top: 16px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--mute);
text-align: center;
}
/* === Print === */
@media print {
body { background: white; }
.hero { background: white; color: var(--ink); border-left-color: var(--accent); }
.hero .eyebrow, .hero .lead, .hero .tldr { color: var(--ink-2); }
.hero h1 { color: var(--ink); }
.hero .tldr { background: var(--accent-bg); }
.hero .tldr strong { color: var(--accent); }
.criterion, .anti, table, .checklist, .process > div, .foot { page-break-inside: avoid; }
h2 { page-break-after: avoid; }
pre { white-space: pre-wrap; word-wrap: break-word; }
a { color: var(--accent); text-decoration: none; }
}
/* === Mobile === */
@media (max-width: 640px) {
.wrap { padding: 20px 16px 40px; }
.hero { margin: -20px -16px 24px; padding: 32px 16px 28px; }
.hero h1 { font-size: 26px; }
h2 { font-size: 20px; margin-top: 40px; }
table { font-size: 13px; }
th, td { padding: 8px 10px; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<p class="eyebrow">Регламент · Ordinis MDM · v1.0</p>
<h1>Что заводить в Ordinis НСИ</h1>
<p class="lead">
Decision framework для владельцев данных: какие справочники стоит выносить в централизованную НСИ Ordinis MDM, а какие держать локально в своём сервисе.
</p>
<div class="tldr">
<strong>TL;DR:</strong> НСИ — для <strong>shared reference data со slow change rate</strong>. Если данные нужны только одному сервису, меняются 100 раз в секунду или это транзакции — это <strong>не</strong> НСИ.
</div>
</header>
<h2><span class="dot"></span>Что <em>стоит</em> заводить в НСИ</h2>
<p>Справочник — кандидат, если он удовлетворяет <strong>минимум 3 из 5</strong> критериев ниже.</p>
<div class="criterion">
<h3>1. Shared между ≥ 2 потребителями</h3>
<p>Данные читают/используют как минимум два разных сервиса (или будут в обозримом roadmap).</p>
<p><strong>Примеры:</strong> Космические аппараты (планировщик миссий + геопортал + биллинг), наземные станции (планировщик + телеметрия + Альтум), классы разрешения (геопортал + ML pipeline).</p>
<div class="hint">Если данные нужны только одному сервису — храни локально. Не плоди интеграционные точки на пустом месте.</div>
</div>
<div class="criterion">
<h3>2. Slow change rate (часы → недели, а не секунды)</h3>
<p>Изменяется редко — раз в неделю / месяц / квартал. Не транзакционные данные.</p>
<table>
<thead><tr><th>Частота изменений</th><th>Это НСИ?</th></tr></thead>
<tbody>
<tr><td>Раз в год — справочник стран</td><td class="yes">✓ да</td></tr>
<tr><td>Раз в квартал — справочник миссий ДЗЗ</td><td class="yes">✓ да</td></tr>
<tr><td>Раз в неделю — справочник антенн (новые станции)</td><td class="yes">✓ да</td></tr>
<tr><td>Раз в час — текущая температура спутника</td><td class="no">✗ нет (телеметрия)</td></tr>
<tr><td>100×/сек — статус заявки на съёмку</td><td class="no">✗ нет (state machine)</td></tr>
</tbody>
</table>
</div>
<div class="criterion">
<h3>3. Нужна validity-window (validFrom / validTo)</h3>
<p>Данные <strong>актуальны на определённый период</strong>. Закрытие/замена записи через bitemporal versioning, не через <code>DELETE</code>.</p>
<p><strong>Примеры:</strong> Тарифы (действуют с 01.01.2026), орбитальные параметры (TLE на конкретную epoch), составы flight crew (период).</p>
<div class="hint">Если данные «текущее состояние и забыли» — может быть просто config или KV store.</div>
</div>
<div class="criterion">
<h3>4. Требуется audit-trail (compliance / governance)</h3>
<p>Нужно знать <strong>кто, когда, почему</strong> изменил запись — для compliance, forensics, отката.</p>
<p>Все справочники в Ordinis по умолчанию имеют <strong>Hibernate Envers</strong> audit log с full history.</p>
<p><strong>Примеры:</strong> Регуляторные классификации, security clearances, тарифные планы (для финансового audit'а).</p>
</div>
<div class="criterion">
<h3>5. Есть схема (structured, validated)</h3>
<p>Данные имеют <strong>стабильную JSON-Schema</strong> с required-полями и валидацией. Не «свободный мешок ключей».</p>
<div class="hint">Если структура каждую неделю меняется и нет SLA на стабильность — может быть это ещё не НСИ, а Notion-like wiki.</div>
</div>
<h2><span class="dot" style="background:var(--pink)"></span>Что <em>не стоит</em> заводить в НСИ</h2>
<div class="anti">
<h3>A. Транзакционные данные</h3>
<p>Заявки, заказы, события, измерения, журналы. Это transactional store (PostgreSQL/Kafka topic application'а), не НСИ.</p>
<p class="symptom">растёт линейно со временем работы системы</p>
</div>
<div class="anti">
<h3>B. Высокочастотные мутации</h3>
<p>&gt; 100 updates/sec — не лезет в audit, забивает Kafka outbox. Для счётчиков и telemetry — Redis / time-series DB / Prometheus.</p>
</div>
<div class="anti">
<h3>C. Производные / вычисляемые данные</h3>
<p>Если значение <strong>вычисляется</strong> из других данных — храни источник, не результат. Кэш производных — в Redis с invalidation.</p>
<p class="symptom">«активные миссии за месяц» — это query, не справочник</p>
</div>
<div class="anti">
<h3>D. PII / sensitive без access control</h3>
<p>Персональные данные физлиц, payment data, security secrets. Ordinis сейчас поддерживает scope (<code>PUBLIC</code> / <code>INTERNAL</code> / <code>RESTRICTED</code>), но <strong>per-record ACL пока в roadmap</strong> (Q4 2026).</p>
<p>Для PII — выделенный сервис с шифрованием и compliance audit, не общий НСИ.</p>
</div>
<div class="anti">
<h3>E. Большие BLOB'ы</h3>
<p>Изображения, PDF, сырые телеметрические дампы. → MinIO/S3 + ссылка в справочнике, но не контент в <code>data</code>.</p>
</div>
<div class="anti">
<h3>F. Данные, нужные только одному сервису</h3>
<p>Если справочник никогда не покинет границу сервиса X — это его внутренний config. Не выноси наружу.</p>
<p class="symptom">boundary test — «может ли потребитель Y разумно захотеть читать это?». Нет → не НСИ</p>
</div>
<h2><span class="dot"></span>Чек-лист перед созданием справочника</h2>
<div class="checklist">
<h3>10 вопросов · ответь "да" / "нет"</h3>
<ol>
<li>Минимум один реальный потребитель кроме создателя? (named system)</li>
<li>Согласованная schema (фиксированный набор полей)?</li>
<li>Change rate &lt; 10/мин в среднем?</li>
<li>Validity windows имеют смысл (или достаточно "current state")?</li>
<li>Audit-trail нужен для compliance / forensics?</li>
<li>Owner назначен? Reviewer pool назначен (для approval-required dicts)?</li>
<li>Bundle подобран? (cuod / geo / comms / delivery — см. existing)</li>
<li>Локализация — нужна? Какие locale supported?</li>
<li>Scope: PUBLIC / INTERNAL / RESTRICTED?</li>
<li>Onboarding plan — кто и когда мигрирует существующие consumers?</li>
</ol>
<div class="verdict">
<strong>≥ 7 yes</strong> — заводи. Если меньше — обсуди с Ordinis team, возможно паттерн не НСИ.
</div>
</div>
<h2><span class="dot"></span>Процесс создания справочника</h2>
<h3>Простой случай (PUBLIC scope, owner = создающий)</h3>
<div class="process">
<div>Owner открывает <code>/dictionaries</code> admin UI → <strong>Создать</strong></div>
<div>Заполняет name, displayName, schema (через UI builder или импорт JSON)</div>
<div>Тестит в <strong>staging</strong> (<code>ordinis.k8s.265.nstart.cloud</code>)</div>
<div>После validation — publish; данные иммутабельно зашиваются в Kafka outbox</div>
<div>Сообщение в <code>#ordinis</code> Slack — потребители подхватывают через <code>/dictionaries</code> listing</div>
</div>
<h3>Approval-required (для критичных)</h3>
<div class="process">
<div>Owner создаёт <strong>schema draft</strong> (не публикует сразу)</div>
<div>Reviewer pool получает email уведомление</div>
<div>Reviewer открывает <code>/reviews</code> → diff → approve/reject</div>
<div>После approval — auto-publish через workflow</div>
</div>
<h3>Onboarding существующих данных (миграция)</h3>
<div class="process">
<div>Owner экспортирует существующий справочник из source-системы в CSV/JSON</div>
<div>Создаёт schema в Ordinis (UI builder)</div>
<div><strong>Bulk import API</strong> (Q4 2026) или manual seed через bundle YAML</div>
<div>Запускает <strong>параллельное чтение</strong> — consumers читают и из source, и из Ordinis, сверяют diff</div>
<div>После недели зелёного diff — переключают consumers на Ordinis exclusive</div>
<div>Декомиссия source: справочник удаляется в source, остаётся в Ordinis</div>
</div>
<h2><span class="dot"></span>Conventions для schema</h2>
<p>Чтобы интеграторам было проще читать твой справочник:</p>
<ul class="conventions">
<li><strong><code>businessKey</code></strong> должен быть human-readable стабильный идентификатор. Не UUID — <code>GS_MOSCOW_A1</code> лучше чем <code>b284f74e-60c8-…</code>.</li>
<li><strong>Required fields</strong> — минимум что нужно для валидности. Не объявляй required то, что может быть пустым в legacy data.</li>
<li><strong><code>x-localized: true</code></strong> на текстовых полях, которые должны переводиться (<code>name</code>, <code>displayName</code>, <code>description</code>).</li>
<li><strong><code>x-references: target_dict.field</code></strong> для FK — Ordinis валидирует и строит граф.</li>
<li><strong><code>x-section</code></strong> — группировка полей в UI editor'е (<code>Основное</code>, <code>Геометрия</code>, <code>Метаданные</code>).</li>
<li><strong><code>enum</code></strong> для closed lists вместо свободной строки (тип КА, статус миссии).</li>
<li><strong><code>format: date-time</code></strong> для timestamps (Ordinis парсит как ISO-8601).</li>
</ul>
<h2><span class="dot" style="background:var(--pink)"></span>Anti-patterns (видели на практике)</h2>
<div class="callouts">
<div><p><strong>Справочник = state machine состояний заявки.</strong> <code>application_status: NEW/APPROVED/REJECTED</code> — это enum в схеме заявки, а не отдельный справочник.</p></div>
<div><p><strong>Справочник под per-user preferences.</strong> Предпочтения юзера X — это его profile в auth-сервисе.</p></div>
<div><p><strong>Справочник вместо feature flag.</strong> Если читается на каждом запросе для decisions — config service (Spring Cloud Config + LaunchDarkly-like).</p></div>
<div><p><strong>«Логирование» через справочник.</strong> Лог событий — Kafka topic + Loki, не НСИ.</p></div>
<div><p><strong>Справочник, чтобы передать данные между двумя своими микросервисами.</strong> Используй внутреннюю DB / message broker, не НСИ.</p></div>
</div>
<h2><span class="dot"></span>Когда сомневаешься — спроси</h2>
<div class="foot">
<strong>Контакты Ordinis team</strong>
<ul>
<li>Slack <code>#ordinis</code></li>
<li>Email: <a href="mailto:zimin.an@nstart.space">zimin.an@nstart.space</a></li>
<li>Office-hours: четверг 15:00 МСК (ad-hoc на DM)</li>
</ul>
<p style="margin-top:12px; color:var(--ink-2); font-style:italic;">Лучше 30 минут на консультацию, чем месяц на отрыв справочника обратно в local store.</p>
</div>
<div class="meta">
Ordinis MDM · v1.0 · 2026-05 · NStart
</div>
</div>
</body>
</html>