Files
mdm-ordinis/docs/integration/dictionary-criteria.html
Zimin A.N. 20763feeb1 docs: HTML-вариант регламента «что заводить в НСИ»
Single-file standalone document (~23 KB). Embed CSS, no external deps.
Brand-consistent с PPTX/Postman — Warm Terracotta palette (#B05A2E),
Georgia headings + Inter body. Responsive (mobile breakpoint 640px),
print-friendly (page-break-inside на cards/tables).

Структура:
- Hero (dark navy + orange stripe + TL;DR callout)
- 5 положительных критериев (green left-border, ✓ glyph, hint boxes)
- 6 anti-patterns (pink left-border, ✗ glyph, symptom captions)
- 10-пунктный чек-лист (terracotta card, numbered orange circles)
- Процесс создания (3 секции × numbered steps)
- Schema conventions
- Final anti-patterns callouts
- Контакты footer

Use case: открыть в браузере, share через corp portal,
print PDF для офлайн-чтения. Markdown-версия остаётся для git-history
и в quickstart Приложение A.
2026-05-20 18:39:22 +03:00

589 lines
23 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>Регламент · Что заводить в 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>