Meridian Office

Meridian Office — архитектура

Раскрывает решения библии (00-foundation.md, разделы 2, 3, 4, 6, 9) до уровня крейтов, типов, протоколов и бюджетов. Версия 0.1 · 2026-10-03. Отступления от библии не допускаются; спорные места — в разделе «Вопросы к библии» в конце.

Содержание: 1 Слои и принципы · 2 Монорепозиторий · 3 Крейты ядра · 4 Модель документа · 5 Отмена и совместная работа · 6 Конвейер рендера · 7 Процессная модель · 8 Файловый ввод-вывод · 9 Скрипты и расширения · 10 Безопасность · 11 Производительность · 12 Платформы · 13 Сборка, CI, тестирование · 14 Лицензирование в архитектуре · 15 Риски и альтернативы · Вопросы к библии.


1. Слои и принципы

1.1 Слои

┌─ Shell ──────────── Qt 6.8 / QML + C++20 ───────────────────────────────────────────────┐
│  окна · ribbon · диалоги · IME · доступность · нативная печать-диалоги · темы · локали   │
│  shell/src/**, shell/qml/**           процессы: meridian-hub, meridian-write, …          │
├─ FFI ────────────── cxx-qt поверх стабильного C ABI (meridian-ffi) ─────────────────────┤
│  QObject-обёртки моделей · холст (QQuickItem ← текстура QRhi) · события · команды        │
├─ Kernel ─────────── Rust, без UI, без сети ─────────────────────────────────────────────┤
│  core · text · layout · grid · calc · scene · raster · chart · math · fields · search    │
│  history (undo + Yrs) · styles · fonts · lingo · license · script-api                    │
├─ Codecs ─────────── Rust, запускаются в песочнице ─────────────────────────────────────┤
│  ooxml · odf · msbinary · rtf · pdf · html/md · image · svg · vsdx · mail · accdb · iwork│
├─ Platform ───────── Rust + C++ (тонкие привязки к ОС) ──────────────────────────────────┤
│  render(Skia) · fonts(DirectWrite/CoreText/fontconfig) · print · fs/cloud · ipc ·        │
│  sandbox · script(QuickJS-ng) · sync(Yrs + WebSocket) · id · update · crash              │
├─ Services ───────── Rust/axum + PostgreSQL (услуги, не часть клиента) ──────────────────┤
│  id · license · billing · portal · update · cloud(ф.6) · telemetry                      │
└──────────────────────────────────────────────────────────────────────────────────────────┘

Правило зависимостей (проверяется cargo-deny bans + скриптом scripts/check-layers.py в CI):

Слой Может зависеть от Не может
Shell FFI, Platform (только C++-части: печать-диалоги, IME) Kernel напрямую, Codecs
FFI Kernel, Platform, Codecs (через meridian-codecs фасад) Shell
Kernel meridian-core, друг от друга по графу §3, std Qt, Skia, сеть (std::net, reqwest), ОС-API кроме файлов/времени
Codecs Kernel (модели), meridian-core Platform, UI, сеть
Platform Kernel, Codecs Shell
Services meridian-core, meridian-license (общие типы ключей) всё клиентское

Запрет сети в ядре формализован: крейты ядра собираются с #![forbid(unsafe_code)] (исключения — meridian-raster (SIMD), meridian-render (Skia), meridian-ffi), в deny.toml для них запрещены reqwest, hyper, tokio-net, ureq, libc (кроме render/raster), а scripts/check-layers.py ищет std::net и std::process в kernel/.

1.2 Принципы

  1. Ядро без UI и без сети. Ядро — библиотека чистых структур данных и алгоритмов. Любой ввод — вызов функции, любой вывод — значение или событие в очереди. Это даёт тестируемость (100 % логики — cargo test), компиляцию в WebAssembly и возможность запускать ядро в песочнице без прав.
  2. Одна модель — много представлений. meridian-text обслуживает абзацы Write, текстовые блоки Slides, заметки Notes, поля форм Base; meridian-grid — листы Sheets, таблицы Write, таблицы данных Chart; meridian-scene — фигуры Draw, слайды, холст Notes, обтекаемые объекты Write. Представление (страница, слайд, лист, холст) — это раскладчик поверх общей модели, а не отдельная модель.
  3. Песочницы по умолчанию. Чужие файлы разбираются в отдельном процессе с урезанными правами (§7), скрипты — в QuickJS с лимитами (§9), внешние ресурсы документов не загружаются без разрешения (§10).
  4. Детерминизм раскладки. Один и тот же документ с одним набором шрифтов даёт байт-в-байт одинаковый список отображения на любой машине и ОС. Это условие для совместной работы (все участники видят одну нумерацию страниц), снимочных тестов корпуса и воспроизводимых отчётов об ошибках. Средства: целые EMU (§3), целочисленный шейпинг, шрифты в комплекте, фиксированный порядок обхода, BTreeMap/детерминированные хэши, запрет f32-накоплений в раскладке (docs/algorithms/kernel-core.md §19).
  5. Каждое изменение — транзакция. Модель меняется только через Transaction с набором Op; из транзакций строятся undo, автосохранение, CRDT и история версий (§4.4, §5).
  6. Чужой файл важнее нашего кода. Неизвестные элементы форматов сохраняются и возвращаются при записи (round-trip), а не теряются; падение кодека — не падение документа.
  7. Границы — явные. FFI — C ABI с версией, IPC — протокол с версией, скрипты — типизированный API с семантическим версионированием. Внутри слоя — свобода рефакторинга, на границах — совместимость.

2. Монорепозиторий

Один репозиторий (meridian/office), один Cargo.toml workspace для клиентского кода, второй — services/ (серверы собираются отдельно и имеют свои зависимости). Оболочка — CMake-проект shell/, который вызывает cargo для ядра. Корпус документов — git-lfs.

meridian-office/
├── Cargo.toml                   workspace: members = ["kernel/crates/*"]; профили; общие lint-правила
├── Cargo.lock                   коммитится (воспроизводимость)
├── rust-toolchain.toml          stable + rustfmt + clippy; точная версия — в CI
├── deny.toml                    cargo-deny: лицензии, уязвимости, запреты (появляется с первой внешней зависимостью)
├── justfile                     setup/build/check/test/fmt/lint/corpus/package-*/docs-site
├── .gitignore  .gitattributes   lfs для корпуса, eol=lf для исходников
├── .github/workflows/
│   ├── ci.yml                   fmt+clippy → ядро на 4 целях → оболочка (по флагу) → корпус (вручную)
│   ├── nightly.yml              санитайзеры, фаззинг 1 ч на цель, бенчмарки с историей (фаза 0)
│   ├── release.yml              теги v* → пакеты MSIX/MSI, DMG, Flatpak/AppImage/deb/rpm, подпись, манифест обновлений
│   └── services.yml             образы сервисов → ghcr.io
├── kernel/
│   ├── crates/                  все крейты ядра, кодеков и платформенных модулей (плоский список, §3)
│   │   ├── meridian-core/       единицы, цвета, геометрия, id, ошибки
│   │   ├── meridian-text/       piece table, атрибуты, абзацы
│   │   ├── meridian-layout/     раскладка строк и страниц
│   │   ├── meridian-grid/       модель листа, ячейки, диапазоны, зависимости формул
│   │   ├── meridian-calc/       лексер/парсер/вычислитель формул
│   │   ├── meridian-scene/      фигуры, группы, коннекторы, анимации
│   │   ├── meridian-raster/     растровые слои, кисти, фильтры
│   │   ├── meridian-chart/      модель и раскладка диаграмм
│   │   ├── meridian-math/       формулы (MathML/LaTeX/UnicodeMath)
│   │   ├── meridian-fields/     поля, оглавления, перекрёстные ссылки, слияние
│   │   ├── meridian-search/     поиск/замена, индекс tantivy
│   │   ├── meridian-history/    транзакции, undo/redo, Yrs-мост
│   │   ├── meridian-styles/     стили, наследование, резолвер с кэшем
│   │   ├── meridian-codecs/     фасад: детект формата, реестр кодеков, песочный запуск
│   │   ├── codec-ooxml/  codec-odf/  codec-msbinary/  codec-rtf/  codec-pdf/  codec-html/
│   │   ├── codec-image/  codec-svg/  codec-vsdx/  codec-mail/  codec-accdb/  codec-iwork/
│   │   ├── meridian-render/     список отображения → Skia (экран, печать, PDF, снимки)
│   │   ├── meridian-fonts/      поиск, подмена, субсеты, кэш шейпинга
│   │   ├── meridian-print/      принтеры, настройки, предпросмотр (тонкая обёртка над render + ОС)
│   │   ├── meridian-lingo/      орфография, переносы, тезаурус, автозамена
│   │   ├── meridian-script/     QuickJS-ng, объектная модель, песочница, транслятор VBA
│   │   ├── meridian-license/    ключи, лицензии, Features
│   │   ├── meridian-id/         клиент аккаунтов, токены в хранилище ОС
│   │   ├── meridian-update/     каналы, манифесты, дельты, проверка подписи
│   │   ├── meridian-sync/       Yrs-синхронизация: WebSocket-клиент, awareness, офлайн-очередь
│   │   ├── meridian-ipc/        протокол Hub ↔ документ ↔ песочница (CBOR, фреймы, shared memory)
│   │   ├── meridian-sandbox/    запуск песочных процессов: seccomp / AppContainer / sandbox-exec
│   │   ├── meridian-assist/     ИИ-функции (фаза 7): локальные модели, облачный адаптер
│   │   └── meridian-ffi/        C ABI + cxx-qt мост; include/meridian.h
│   ├── fuzz/                    cargo-fuzz: targets/<codec>.rs, корпус затравок → tests/corpus/fuzz-seeds
│   └── benches/                 criterion-бенчмарки, общие для нескольких крейтов
├── shell/
│   ├── CMakeLists.txt           Qt 6.8, qt_add_qml_module, импорт ядра
│   ├── src/                     C++: main, окна, платформенные хуки (CSD, IME, печать, a11y)
│   │   ├── account/             диалоги входа/активации, менеджер лицензий (библия §9.5)
│   │   ├── canvas/              DocumentCanvasItem (QQuickItem + QRhi текстура)
│   │   ├── commands/            реестр команд, горячие клавиши, палитра
│   │   └── platform/{win,mac,linux}/
│   ├── rust/                    крейт meridian-shell-bridge: #[cxx_qt::bridge] QObject-модели
│   ├── qml/                     Main.qml, Ribbon/, Panels/, Dialogs/, Backstage/, Style/ (стиль «Meridian»)
│   ├── assets/                  иконки Meridian Glyphs (SVG), шрифты Inter/JetBrains Mono, темы
│   └── tests/                   Qt Test + Squish-сценарии
├── apps/                        конфигурация приложений поверх общей оболочки (не отдельный код UI)
│   ├── hub/        write/       sheets/      slides/      draw/       paint/
│   ├── pdf/        math/        notes/       mail/        base/       forms/
│   └── <app>/      app.toml (id, цвет, расширения, вкладки ribbon, модули ядра), ribbon.json,
│                   commands.json, shortcuts/{office,libreoffice,meridian}.json, templates/, locales/
├── services/                    серверы (отдельный Cargo workspace), см. services/README.md
│   ├── Cargo.toml  docker-compose.yml  README.md
│   ├── id-service/  license-service/  billing-service/  portal/  update-service/  cloud-service/  telemetry-service/
│   └── infra/                   postgres init, миграции, otel-collector, gateway
├── codecs/                      НЕ код: спецификации и заметки по форматам
│   ├── ooxml/ odf/ msbinary/ …  конспекты спецификаций, таблицы соответствия элементов, известные отклонения Word/Excel
│   └── mop/                     XSD + RELAX NG схем MOP, примеры, changelog схем
├── tools/
│   ├── convert/                 meridian-convert: CLI конвертер/рендер (для корпуса и пользователей)
│   ├── imgdiff/                 meridian-imgdiff: сравнение снимков с порогом
│   ├── keygen/                  выпуск ключей и подпись лицензий (только у владельца, HSM/YubiKey)
│   ├── corpus-ref/              генерация эталонов через приложения-источники (Word/LO) по сети
│   ├── mop-lint/                проверка пакетов MOP по схемам
│   └── ts-types/                генерация .d.ts MeridianScript из Rust-объявлений
├── scripts/                     bootstrap.sh, corpus-test.sh, build-site.sh, check-layers.py, release/*
├── tests/
│   ├── corpus/                  документы + эталоны (git-lfs), README.md
│   ├── integration/             сквозные тесты: открыть → изменить → сохранить → сравнить
│   └── ui/                      сценарии Squish / Qt Test уровня приложения
├── docs/                        00-foundation.md, 03-architecture.md, algorithms/, apps/, 10-licensing-accounts.md, …
├── mockups/                     HTML-макеты экранов на токенах MDS (mds.css)
├── spec/                        формальные спецификации: MOP (ZIP/XML), MeridianScript API, IPC, .mxt, протокол sync
├── packaging/
│   ├── windows/                 WiX 4 (MSI), MSIX-манифест, подпись EV, preview handler, IFilter
│   ├── macos/                   DMG, нотаризация, Quick Look, Spotlight-импортер, App Store entitlements
│   ├── linux/                   Flatpak-манифест, AppImage, deb/rpm, desktop-файлы, MIME, иконки
│   └── fonts/                   лицензии и список шрифтов в комплекте
└── CHANGELOG.md  CONTRIBUTING.md  SECURITY.md  LICENSE*

Правила репозитория:


3. Крейты ядра

Граф зависимостей направлен сверху вниз: meridian-core ни от чего не зависит; кодеки зависят от моделей, но модели не знают о кодеках; meridian-render знает о моделях только через DisplayList.

Крейт Ответственность Ключевые типы и трейты Зависимости
meridian-core единицы (EMU), цвета, геометрия, UUIDv7, ошибки, арены, интернирование Length(i64), Angle, Color, Point/Size/Rect/Insets, Uuid, DocId/NodeId/StyleId, Error, Arena<T>, Interner —
meridian-text текст абзаца: piece table, атрибутные спаны, сегментация (графемы, слова, предложения), bidi-уровни PieceTable, AttrSpans, Attrs, AttrKey, Paragraph, TextRange, Segmenter core
meridian-styles стили абзаца/символа/таблицы/фигуры, наследование, темы (цвета/шрифты темы OOXML), резолвер с кэшем StyleSheet, Style, StyleKind, ResolvedStyle, StyleResolver, Theme, trait Styled core, text
meridian-layout раскладка строк (шейпинг через rustybuzz), переносы, абзацы, колонки, страницы, обтекание, таблицы в потоке LayoutEngine, trait LayoutHost, LineBreaker, ShapedRun, PageLayout, Flow, FloatPlacer, TableLayout core, text, styles, fonts
meridian-grid модель листа: ячейки, строки/столбцы, диапазоны, объединения, форматы чисел, граф зависимостей, пересчёт Workbook, Sheet, CellAddr, Range, CellValue, NumberFormat, DependencyGraph, Recalc core, calc, text, styles
meridian-calc лексер, Pratt-парсер, AST, вычислитель, библиотека функций (500+ к фазе 2) Expr, CellRef, Value, CalcError, trait CellProvider, FunctionRegistry core
meridian-scene сцена фигур: иерархия, трансформации, геометрия пресетов DrawingML, коннекторы, анимации Slides Scene, Shape, ShapeKind, Transform2D, Connector, Timeline, trait HitTest core, text, styles, layout
meridian-raster растровое ядро Paint: слои, кисти, выделения, фильтры, цветовые пространства, тайлы 256² Image, Layer, TileGrid, Brush, Selection, trait Filter, ColorSpace core
meridian-chart модель диаграмм (OOXML chart + ODF chart), раскладка в сцену, оси, легенды, подписи Chart, Series, Axis, ChartLayout, ChartKind core, scene, grid, styles
meridian-math формулы: MathML Core, разбор LaTeX и UnicodeMath, раскладка по правилам TeX (STIX Two) MathNode, MathParser, MathLayout, MathFont core, layout, fonts
meridian-fields поля Write: нумерация страниц, оглавление, ссылки, перекрёстные ссылки, даты, слияние, вычисление по meridian-calc Field, FieldKind, FieldUpdater, MergeSource core, text, calc
meridian-search поиск/замена (regex, нормализация Unicode), полнотекстовый индекс Hub/Notes/Mail на tantivy, подсветка Finder, SearchQuery, Index, Highlighter core, text
meridian-history транзакции, OpLog, UndoManager, мост в Yrs (yrs::Doc), снимки, версии Transaction, Op, OpLog, UndoManager, CrdtDoc, Snapshot, Version core, text, grid, scene, yrs
meridian-codecs фасад: детект формата, реестр, потоковый ZIP, запуск кодека в песочнице, прогресс/отмена trait Importer, trait Exporter, FormatId, detect(), ZipReader/ZipWriter, CodecJob core, все модели, ipc
codec-ooxml docx/xlsx/pptx/vsdx-общее (OPC, отношения, DrawingML, темы) + Word/Excel/PowerPoint-специфика OpcPackage, DocxReader/Writer, XlsxReader/Writer, PptxReader/Writer, DrawingMl codecs, модели, quick-xml
codec-odf odt/ods/odp/odg/odf: ODF 1.3, стили автоматические/общие, шифрование ODF OdfPackage, OdtReader/Writer, … codecs, модели
codec-msbinary doc/xls/ppt (MS-DOC, MS-XLS, MS-PPT), CFB-контейнер, OLE-объекты, старые шифрования CfbFile, DocReader, XlsReader, PptReader (чтение; запись — только doc/xls базово) codecs, модели
codec-rtf RTF 1.9.1: чтение/запись, кодовые страницы, вложенные таблицы RtfReader, RtfWriter, RtfToken codecs, text
codec-pdf чтение (структура, текст, аннотации, формы) и запись (из DisplayList, PDF/A-2b, PDF/UA) PdfDocument, PdfPage, PdfWriter, PdfAnnotation codecs, render
codec-html HTML/XHTML/Markdown/EPUB/FB2: чтение с CSS-подмножеством, запись HtmlReader, HtmlWriter, MarkdownReader/Writer, EpubWriter codecs, text, styles
codec-image PNG/JPEG/WebP/GIF/BMP/TIFF/ICO/HEIC/AVIF/PSD/ORA, EXIF, ICC ImageDecoder, ImageEncoder, ImageInfo codecs, raster
codec-svg SVG 1.1/2 → сцена и обратно, растрирование через render SvgReader, SvgWriter codecs, scene, render
codec-vsdx Visio: vsdx (OPC), vsd (бинарный, чтение), мастера, коннекторы VsdxReader/Writer, VsdReader codecs, scene, ooxml
codec-mail EML/MBOX/ICS/VCF, PST/OST (чтение), MSG, LDIF MimeMessage, MboxReader, PstReader, ICalendar, VCard codecs, text
codec-accdb ACCDB/MDB (Jet/ACE): таблицы, индексы, запросы (чтение); ODB (чтение) JetFile, AccdbReader, OdbReader codecs
codec-iwork Pages/Numbers/Keynote (IWA, Snappy, protobuf-схемы) — чтение IwaArchive, PagesReader, NumbersReader, KeynoteReader codecs, модели
meridian-render DisplayList, инвалидация, тайлы, Skia-бэкенды (Metal/D3D12/Vulkan/GL/CPU), печать/PDF/снимки DisplayList, Cmd, TileCache, trait Backend, SkiaBackend, Renderer, SnapshotRenderer core, fonts, skia-safe
meridian-fonts база шрифтов (fontdb + системные API), подмена по метрикам, субсеты для PDF, кэш шейпинга FontDb, FontId, FontQuery, Fallback, ShapeCache, Subsetter core, fontdb, rustybuzz, ttf-parser
meridian-print принтеры и их возможности, настройки задания, предпросмотр, спулинг (CUPS/WinSpool/NSPrint) Printer, PrintJob, PaperSize, PrintSettings core, render
meridian-lingo Hunspell-словари, переносы, тезаурус, автозамена, определение языка, LanguageTool-клиент (опц.) SpellChecker, Hyphenator, Thesaurus, AutoCorrect, LangDetect core, text, hunspell-rs, hyphenation
meridian-script QuickJS-ng, объектная модель meridian.*, события, права, лимиты, отладчик, запись макросов, транслятор VBA ScriptHost, Sandbox, Permission, ApiRegistry, Debugger, VbaTranslator core, модели, history, rquickjs
meridian-license ключи MRDN-… по spec/license/license-key-format.md, Features (u128, имена флагов), License (.mlic), сроки, отпечаток устройства, Verifier, SHA-256/HMAC на std KeyFields, DecodedKey, Features, Feature, Edition, License, LicenseStatus, trait Verifier — (без сети, только std)
meridian-id OAuth/OIDC-поток (loopback + PKCE), токены в Keychain/DPAPI/Secret Service, профиль, устройства Session, TokenStore, trait SecretStore, Account core, license
meridian-update каналы, манифест (Ed25519, TUF-роли), дельты zstd+bsdiff, проверка, откат UpdateManifest, Channel, Delta, Updater core
meridian-sync WebSocket-клиент Yrs-протокола, awareness, офлайн-очередь в SQLite, ACL-клиент SyncClient, Awareness, Outbox, SyncState core, history
meridian-ipc фреймы, CBOR, запрос/ответ/поток, shared memory, таймауты Frame, Message, Channel, SharedBuffer, trait Service core, ciborium
meridian-sandbox запуск дочерних процессов с ограничениями (seccomp-bpf, AppContainer, sandbox-exec), лимиты памяти SandboxSpec, Sandboxed, Limits core, ipc
meridian-assist ИИ: локальные модели (llama.cpp через ffi, опц.), облачный адаптер, промпты по приложениям (фаза 7) Assistant, trait Model, Task core, text
meridian-ffi C ABI (meridian.h), cxx-qt мост (shell/rust), конвертация ошибок, перехват паник MeridianDocument, MeridianStatus, meridian_*() все нужные

3.1 Единицы измерения: целые EMU

Решение: внутренняя длина — Length(i64) в EMU (1 pt = 12 700, 1 in = 914 400, 1 mm = 36 000, 1 twip = 635).

Критерий f64 pt целые EMU (выбрано)
Детерминизм между машинами нет: порядок суммирования и FMA дают разные биты да: целочисленная арифметика
OOXML (EMU, twips, half-points, 1/8 pt) конвертация с округлением в обе стороны twips/half-points — точно; 1/8 pt — ±0,5 EMU, круговое преобразование точное (тест eighth_points_round_trip)
ODF (cm/in/pt с десятичными) точно для десятичных ошибка ≤ 14 нм, при 4 знаках ODF — без потерь (тест odf_round_trip_four_decimals)
Шрифтовые метрики (units × size / upem) плавающие Length::mul_div с округлением «половина от нуля» — одинаково на всех платформах
Диапазон — ±2⁶³ EMU ≈ ±10¹⁰ км; переполнение исключено, промежуточные произведения — в i128
Скорость равна равна (i64), SIMD-дружелюбно

Рендер переводит EMU в пиксели устройства (f32) только на последнем шаге (§6.6); в кэшах раскладки хранятся EMU. Углы — Angle(i32) в 1/60 000° (DrawingML), проценты — Permille(i32)/Percent100k(i32) по формату.

3.2 meridian-history: undo и CRDT в одном крейте

Один крейт отвечает и за локальную отмену, и за совместную работу, потому что обе функции строятся над одним журналом операций и одной структурой Yrs. Локальная сессия без облака тоже использует yrs::Doc: это даёт бесплатную историю версий в history/ и единый код для undo. Подробно — §5.


4. Модель документа

4.1 Узлы и атрибуты

Все приложения описывают документ как дерево узлов, где узлы-листья специализированы по доменам:

/// Общий узел дерева документа / Generic document tree node.
pub struct Node {
    pub id: NodeId,                 // UUIDv7, стабилен на всю жизнь узла / stable for the node's lifetime
    pub kind: NodeKind,
    pub attrs: Attrs,               // прямое форматирование / direct formatting
    pub style: Option<StyleId>,     // именованный стиль / named style
    pub children: Vec<NodeId>,      // порядок детей / ordered children
    pub parent: Option<NodeId>,
    pub ext: Option<Box<ExtData>>,  // неизвестные элементы чужих форматов для round-trip / foreign extension data
}

pub enum NodeKind {
    Document(DocMeta),
    Section(SectionProps),          // Write: поля, колонки, колонтитулы / page setup
    Paragraph(ParagraphRef),        // → meridian_text::Paragraph в арене
    Table(TableProps), TableRow, TableCell(CellProps),
    Shape(ShapeRef),                // → meridian_scene::Shape
    Image(ImageRef),                // → ресурс в media/
    Sheet(SheetRef),                // → meridian_grid::Sheet
    Slide(SlideProps), Layout(LayoutProps), Master(MasterProps),
    Page(PageProps),                // Notes: свободный холст
    Field(FieldRef), Footnote, Comment(CommentProps), Bookmark(BookmarkProps),
    Custom(TypeId),                 // расширения / extensions
}

Узлы хранятся в арене Arena<Node> (meridian-core), индексируются HashMap<NodeId, ArenaIndex> (FxHash с фиксированным seed — детерминированная итерация не требуется, для неё есть children). Тяжёлые данные (текст абзаца, растры, листы) лежат в доменных аренах и адресуются *Ref-индексами — это держит Node маленьким (≤ 128 байт) и даёт локальность при обходе.

Attrs — упорядоченная карта AttrKey → AttrValue (meridian-text::Attrs, BTreeMap); ключи — перечисление с Custom(InternedStr) для расширений. Один тип атрибутов для символов, абзацев, фигур и ячеек: различается набор ключей, не контейнер. Значения: Bool, Int(i64) (EMU, ARGB, перечисления), Text, в полной версии — Length, Color, Enum(u16), Ref(NodeId), List(Vec<AttrValue>).

4.2 Резолвер стилей

pub struct StyleResolver<'a> {
    sheet: &'a StyleSheet,
    theme: &'a Theme,
    cache: HashMap<ResolveKey, Arc<ResolvedStyle>>,  // ResolveKey = (style chain hash, attrs hash, context)
}
impl StyleResolver<'_> {
    /// Полное вычисленное форматирование узла с учётом цепочки наследования:
    /// default → базовые стили (basedOn) → стиль узла → условное форматирование (таблицы) →
    /// прямые атрибуты родителей (для символов: абзац → run) → прямые атрибуты узла.
    /// Fully resolved formatting following the inheritance chain.
    pub fn resolve(&mut self, node: &Node, ctx: &ResolveContext) -> Arc<ResolvedStyle>;
}

Кэш инвалидируется по версии StyleSheet (монотонный u64, растёт при любом изменении стиля) и хранит Arc<ResolvedStyle> — абзацы одного стиля разделяют одну структуру. Алгоритм и анализ сложности — algorithms/kernel-core.md §2.

4.3 Общий текстовый движок

Приложение Что использует Контейнер Раскладчик
Write Paragraph в потоке секций Node::Section → Paragraph meridian-layout::Flow (страницы, колонки, обтекание)
Slides текст фигур, заметки докладчика Shape::text: TextBody meridian-layout::BoxLayout (вертикальное выравнивание, автоподбор)
Draw подписи фигур, текст по контуру Shape::text BoxLayout, PathLayout
Notes блоки на холсте Page → Block(TextBody) BoxLayout с автовысотой
Sheets текст ячейки (rich text), примечания CellValue::Rich(TextBody) CellLayout (однострочный/перенос)
Base / Forms подписи, многострочные поля FormControl::Text(TextBody) BoxLayout
Mail тело письма (HTML-подмножество) Document Write-типа Flow без страниц

TextBody = Vec<Paragraph> + локальные стили; Paragraph = PieceTable + AttrSpans + абзацные Attrs. Движок один, различается LayoutHost — трейт, через который раскладчик спрашивает доступную ширину, обтекаемые объекты и позицию разрыва страницы/колонки.

4.4 Идентификаторы и адресация

4.5 Операции, транзакции, снимки

pub enum Op {
    InsertNode { parent: NodeId, index: u32, node: Box<Node> },
    RemoveNode { id: NodeId },
    MoveNode { id: NodeId, new_parent: NodeId, index: u32 },
    SetAttr { id: NodeId, key: AttrKey, value: Option<AttrValue> },
    SetStyle { id: NodeId, style: Option<StyleId> },
    TextInsert { para: NodeId, at: u32, text: Arc<str> },
    TextDelete { para: NodeId, range: Range<u32> },
    TextAttr { para: NodeId, range: Range<u32>, key: AttrKey, value: Option<AttrValue> },
    CellSet { addr: CellAddr, value: Option<CellValue> },
    CellRangeOp { range: Range, op: RangeOp },     // вставка/удаление строк, заливка, сортировка
    StyleEdit { id: StyleId, patch: StylePatch },
    Composite(Vec<Op>),                            // атомарная группа (напр. «вставить таблицу»)
}

pub struct Transaction {
    pub id: Uuid,              // v7: порядок во времени / time-ordered
    pub origin: Origin,        // Local { command: &'static str } | Remote { client } | Undo | Redo | System
    pub ops: Vec<Op>,
    pub inverse: Vec<Op>,      // заполняется при применении / filled while applying
    pub cursor_before: Option<Selection>,
    pub cursor_after: Option<Selection>,
    pub timestamp_ms: u64,
}

Инварианты: модель меняется только через Document::apply(tx); каждая Op обратима (inverse строится в момент применения, когда известны старые значения); Composite применяется целиком или откатывается. После применения транзакция уходит в три места: UndoManager, OpLog (журнал автосохранения §8.3) и CrdtDoc (если документ открыт с историей или в совместном режиме, §5).

Снимки. Snapshot — неизменяемое состояние документа для фоновых задач (раскладка, индексирование, экспорт) без блокировки редактирования: арены узлов хранятся как персистентные структуры (im::Vector-подобный вектор с разделением структуры, собственная реализация в meridian-core::persistent), PieceTable клонируется по ссылке (буферы Arc<str>, список кусков — O(p) копия). Снимок берётся за O(log n), живёт сколько нужно фоновой задаче, и результат раскладки сопоставляется с текущей версией по tx.id: если за время раскладки прошли новые транзакции, затронутые области пересчитываются инкрементально (§6.2).


5. Отмена/повтор и совместная работа

5.1 Архитектура на Yrs

yrs::Doc — источник истины для совместно редактируемых данных; модель в аренах (§4) — материализованное представление, которое обновляется из событий Yrs (observe_deep) и из локальных транзакций. Локальная транзакция применяется к модели и к yrs::Doc в одной yrs::Transact; удалённое обновление применяется к yrs::Doc, а порождённые события транслируются в Op и применяются к модели с Origin::Remote.

Отображение модели на типы Yrs:

Приложение Данные Тип Yrs Примечание
Все дерево узлов XmlFragment/XmlElement (nodes) атрибуты узла — атрибуты элемента; id — атрибут id; порядок детей — порядок элементов
Write/Slides/Notes текст абзаца XmlText внутри элемента p атрибутные спаны — форматы XmlText (format(range, attrs)), это даёт символьное слияние
Все стили Map (styles): StyleId → Map изменение одного свойства — одна запись
Sheets ячейки Map (cells): ключ "<sheet>:<row>:<col>" только для изменённых ячеек см. 5.2
Sheets структура листов, строки/столбцы Array (sheets) из Map; Map (rows/cols) ширины/высоты/скрытие
Draw/Slides фигуры элементы XmlFragment с атрибутами геометрии трансформация — один атрибут xfrm (CBOR-строка), чтобы перемещение было одной записью
Paint слои Array (layers) из Map (метаданные) пиксели не в CRDT: Paint не поддерживает совместную работу (библия не требует)
Все комментарии, закладки Map (comments), Map (bookmarks) права «комментатор» — только эти карты
Все метаданные Map (meta)

5.2 Таблицы ячеек в CRDT без взрыва памяти

Проблема: лист на 1 М ячеек как 1 М записей Map — ~100 Б на запись в Yrs, 100 МБ только на структуру CRDT. Решение — базовый снимок + дельты:

  1. При открытии файла (импорт xlsx или MOP без истории) данные листа загружаются в meridian-grid (колоночные арены, ~16 Б/ячейка) и получают base_hash (BLAKE3 колонок).
  2. В yrs::Doc хранятся только ячейки, изменённые после базы: cells: Map<"s:r:c", CellData>; удалённые — значение Null. Чтение ячейки: сначала дельта, потом база.
  3. Структурные операции (вставка строки) записываются как операции над картой structure с LWW-семантикой (Array из {op, index, count, ts}) и применяются к базе детерминированно всеми участниками в порядке (ts, client). Конфликт двух одновременных вставок строк решается этим порядком — обе вставки применяются, адреса в дельтах переписываются через RowMap (сжатая таблица сдвигов, O(log n) на запрос).
  4. Компактация: когда дельта > 20 % базы или по команде «сохранить», дельта вливается в базу, пишется новый base, в CRDT вставляется маркер base_hash, и записи дельты до маркера удаляются (Map::remove), что после GC Yrs освобождает память. Участники с другим base_hash докачивают новую базу у сервера/файла.

Эта схема держит Yrs-структуру пропорциональной числу правок, а не размеру таблицы; совместная работа над 1 М-ячеек листом стоит столько же, сколько над пустым.

5.3 Локальная отмена поверх CRDT

UndoManager Yrs отслеживает только операции с Origin::Local (tracked_origins = {LOCAL}), поэтому «Отменить» никогда не откатывает чужие правки. Группировка:

Для данных вне Yrs (пиксели Paint, кэш раскладки) UndoManager ведёт параллельный стек inverse-операций из транзакций; обе половины объединяются в UndoEntry { yrs_scope, inverse_ops, cursor }, чтобы одно «Отменить» было консистентным. Глубина стека — 1000 записей или 64 МБ памяти (старые срезаются).

5.4 Курсоры и присутствие

Протокол awareness (как в y-protocols): состояние клиента { user: {id, name, color}, cursor: StickyIndex, selection: (StickyIndex, StickyIndex), app_state: {sheet, slide} }, отправка по изменению, но не чаще 20 Гц (coalescing), TTL 30 с, удаление по тайм-ауту. Цвета — из палитры 8 цветов MDS по хэшу user.id. Удалённые курсоры рисуются в слое оверлеев (§6.4) и не влияют на раскладку.

5.5 История версий

5.6 history/ в пакете MOP

history/
├── log.yrs            обновления Yrs (encoding v2, lib0), append-only, чанки по 1 МБ: log.0001.yrs …
├── base/              базовые снимки Sheets (колоночный формат, zstd), по хэшу: <blake3>.cols
├── versions.cbor      список Version (имя, автор, время, snapshot)
└── clients.cbor       соответствие client_id → пользователь (для отображения авторства)

manifest.xml держит history-state="<hash state vector>"; при открытии проверяется, что XML-содержимое соответствует состоянию log.yrs. Несовпадение (файл правили без истории, например другим редактором) → XML считается истиной, история помечается stale и начинается заново с новой базы; пользователю показывается уведомление. Файл без history/ открывается как обычный документ, история стартует при первом изменении.

5.7 Сервер синхронизации (фаза 6)


6. Конвейер рендера

6.1 Список отображения

pub enum Cmd {
    Save, Restore,
    Transform(Affine),                                // 2×3, f32 — только на этом уровне / f32 only here
    Clip(ClipShape),                                  // Rect | RRect | Path, антиалиасинг флагом
    Layer { alpha: f32, blend: BlendMode, bounds: RectPx },
    FillRect { rect: RectPx, paint: PaintId },
    Path { path: PathId, paint: PaintId },            // пути интернированы / interned paths
    GlyphRun { font: FontId, size: f32, glyphs: Range<u32>, origin: PointPx, paint: PaintId, flags: TextFlags },
    Image { image: ImageId, src: RectPx, dst: RectPx, sampling: Sampling },
    Shader { rect: RectPx, shader: ShaderId },       // градиенты, узоры
    Marker(MarkerKind, NodeId),                       // границы узлов — для хит-теста и инвалидации
}
pub struct DisplayList {
    cmds: Vec<Cmd>,
    glyphs: Vec<Glyph>,          // (glyph_id, x, y) плотно / dense
    paints: Vec<Paint>, paths: Vec<Path>,
    index: RTree<(RectPx, Range<u32>)>,  // пространственный индекс диапазонов команд по узлам
    bounds: RectPx,
}

Раскладка производит DisplayList на страницу/слайд/видимый диапазон листа в координатах документа (EMU → f32 px при масштабе 1:1 и 96 dpi; масштаб и HiDPI применяются трансформацией при рисовании, глифы перехинтовываются бэкендом). Список сериализуем (CBOR) — это формат снимков для тестов и обмен с песочным процессом, если рендер документа когда-нибудь выносится из процесса окна.

6.2 Инвалидация и грязные регионы

Цепочка: Transaction → затронутые NodeId → раскладчик помечает блоки (LayoutBlock = абзац/таблица/фигура) грязными → инкрементальная раскладка пересчитывает только от первого грязного блока до точки, где позиции последующих блоков совпали с прежними (ранний выход; для Write — обычно в пределах страницы) → список отображения страницы пересобирается из кэшированных фрагментов блоков (BlockDL) → DirtyRegion (объединение старых и новых bbox изменённых блоков) → инвалидация тайлов.

6.3 Тайловый кэш

6.4 Слои

Слой Содержимое Обновление
Документ тайлы страниц, фон холста, тени страниц по инвалидации
Выделение текстовое выделение, выделение ячеек, рамки фигур каждый кадр, векторно, дёшево
Оверлеи курсоры (свой и чужие), маркеры комментариев, линейки, направляющие, подсказки каждый кадр
Интерактив перетаскиваемые объекты, рамки изменения размера во время жеста, поверх всего

Слои композитируются в DocumentCanvasItem (QQuickItem): документ — текстура из TileCache, остальные — DisplayList малого размера, рисуемые тем же бэкендом в ту же цель кадра (QRhi QRhiTexture/QSGTexture).

6.5 Текст

6.6 Бэкенды

ОС GPU (основной) Резерв Выбор
Windows 10/11 D3D12 (Skia Ganesh/Graphite) D3D11 через ANGLE→GL, затем CPU по feature level и чёрному списку драйверов
macOS 13+ Metal CPU Metal всегда доступен на поддерживаемых Mac
Linux Vulkan 1.1 OpenGL 3.3 / GLES 3.0, затем CPU по vkEnumeratePhysicalDevices, MESA_*, чёрный список

Интеграция с Qt: QRhi создаёт устройство (D3D12/Metal/Vulkan/OpenGL), Skia получает тот же GrDirectContext через нативные хэндлы (QRhi::nativeHandles()), тайлы остаются на GPU без копирования. Резервный путь — Skia CPU в QImage → QSGTexture (загрузка ≤ 4 МБ/кадр при 60 к/с на обычный экран — приемлемо).

HiDPI: devicePixelRatio — любой дробный (1,25; 1,5; 1,75; 2; 3). Размеры тайлов — в пикселях устройства; тонкие линии (границы таблиц 0,5 pt) привязываются к пиксельной сетке (snap = round(x·dpr)/dpr), чтобы не размываться; при печати привязка отключена.

6.7 Печать и PDF из того же списка

Renderer::print(job): тот же DisplayList страниц → SkDocument PDF (SkPDF::MakeDocument, шрифты субсетируются meridian-fonts::Subsetter, PDF/A-2b при экспорте) → macOS/Linux: PDF напрямую в CUPS/NSPrint; Windows: XPS через SkXPS в XPS Print API; для принтеров без XPS/PDF-пути — растеризация 600 dpi полосами по 64 МБ в GDI (StretchDIBits). Предпросмотр печати — тот же список с эмуляцией полей принтера.

6.8 Снимки для тестов

SnapshotRenderer рендерит DisplayList CPU-бэкендом при 150 dpi в PNG (sRGB, без альфы) с фиксированными настройками (без хинтинга, grayscale AA, шрифты только из комплекта) — одинаково на всех ОС. Второй артефакт — текстовая сериализация DisplayList (.dl.txt, команды по строкам, числа с 2 знаками) для дифф-дружелюбного сравнения раскладки без растра.


7. Процессная модель

meridian-hub (брокер, 1 экземпляр на пользователя)
 ├─ недавние файлы, шаблоны, облако, обновления, продление лицензий, буфер обмена (история), индекс поиска
 ├─ реестр окон: spawn/monitor процессов документов, восстановление после сбоя
 ├─ meridian-write  (окно 1: документ A)   ── IPC ──┐
 ├─ meridian-write  (окно 2: документ B)             ├─ codec-host (песочница, по задаче импорта/экспорта)
 ├─ meridian-sheets (окно 3: книга C)                ├─ script-host (Python-плагин, по сессии)
 └─ …                                                └─ render-host (опц., для «защищённого просмотра» PDF)

7.1 Роли процессов

Процесс Исполняемый Права Живёт
Hub meridian-hub обычные пользователя; единственный с сетью (облако, обновления, лицензии, телеметрия) пока открыто хоть одно окно + 60 с
Окно документа meridian-<app> (один бинарник оболочки, режим по аргументу) обычные, без исходящей сети (sync — через Hub-прокси) пока открыто окно
Codec-host meridian-codec-host песочница: только чтение входного файла (fd передан), запись во временный fd, без сети, без exec одна задача (импорт/экспорт), ≤ 5 мин
Script-host meridian-python-host песочница + права по манифесту расширения сессия скрипта

Один процесс на окно — изоляция сбоев (crash одного документа ≠ потеря других), независимые heap-лимиты, параллельность без общих блокировок. Цена — память (§11): пустое окно ≤ 150 МБ, из них ~60 МБ — Qt/QML.

7.2 Песочница кодеков

ОС Механизм Что запрещено
Linux seccomp-bpf (allowlist: read/write/pread/pwrite/mmap/munmap/mremap/brk/futex/clock_gettime/exit_group/sigreturn/madvise), `unshare(CLONE_NEWNET NEWPID
Windows AppContainer (без capabilities) + Job Object (память, CPU, kill-on-close) + ProcessMitigationPolicy (DEP, ASLR, CFG, без дочерних процессов, без Win32k) файловая система кроме переданных хэндлов, сеть, реестр
macOS sandbox_init с профилем (version 1)(deny default)(allow file-read-data (literal "<in>"))(allow file-write-data (literal "<tmp>"))(allow sysctl-read)(allow mach-lookup (global-name "com.apple.fonts")); для сборки App Store — XPC-сервис с собственными entitlements всё остальное

Кодек получает входной файл как переданный дескриптор (SCM_RIGHTS / DuplicateHandle / Mach port) и возвращает результат двумя путями: модель — CBOR-поток ModelChunk в shared memory; медиа (картинки) — как есть в отдельный shared buffer; оболочка сама декодирует изображения в своём процессе? Нет: декодирование растров — тоже в codec-host (codec-image), в основной процесс попадают только уже проверенные RGBA/сжатые тайлы с заголовком известного формата.

7.3 IPC: собственный протокол на CBOR

Решение: собственный фреймированный протокол + CBOR, транспорт — Unix domain sockets (Linux/macOS), named pipes (Windows), большие буферы — shared memory.

Критерий Cap'n Proto CBOR + свой фрейминг (выбрано)
Zero-copy да нет для сообщений (они ≤ 64 КБ), да для буферов (shared memory)
Схема и генератор внешний компилятор, отдельная сборка типы Rust + serde, схема = код; версионирование полями Option и #[serde(default)]
Единообразие ещё один формат CBOR уже используется в .mlic, history/, .mxt
Обе стороны Rust избыточно достаточно
Отладка бинарные дампы cbor2diag, читаемые логи

Фрейм: u32 len | u32 msg_id | u16 kind (Request=1, Response=2, Stream=3, StreamEnd=4, Event=5, Cancel=6) | u16 flags | CBOR payload. Сообщение — enum Message с #[serde(tag = "t")]; v: u16 версии протокола в Hello. Таймауты: запросы 30 с (кодеки — 5 мин с heartbeat Progress), heartbeat 2 с, 3 пропуска → процесс считается зависшим и убивается.

Shared memory: SharedBuffer::new(len) → Linux memfd_create + F_SEAL_SHRINK|F_SEAL_GROW; macOS shm_open + немедленный shm_unlink (имя одноразовое); Windows CreateFileMapping(INVALID_HANDLE_VALUE); передача дескриптора — через управляющее сообщение канала. Данные в буфере имеют заголовок { magic, version, kind, len, blake3 }, получатель проверяет хэш перед разбором.

7.4 Восстановление после сбоя

7.5 Лимиты памяти

Процесс Мягкий лимит Жёсткий лимит Действие
Окно документа 2 ГБ (предупреждение, сброс кэшей) нет (ОС) MemoryPressure::High → тайлы, шейпинг-кэш, снимки сбрасываются; большие изображения — ленивые мип-уровни
Codec-host — 2 ГБ (RLIMIT_AS / Job Object / setrlimit), для файлов > 200 МБ — 4 ГБ убийство процесса, ошибка пользователю
Script-host 512 МБ по умолчанию, из манифеста до 2 ГБ то же исключение в скрипт OutOfMemory, затем убийство
QuickJS в окне 64 МБ на контекст (JS_SetMemoryLimit) — исключение InternalError: out of memory

8. Файловый ввод-вывод

8.1 Потоковый ZIP и mmap

Собственный читатель ZIP в meridian-codecs::zip (поверх zip-крейта только для deflate-кодека): разбор центрального каталога с конца файла (поддержка ZIP64, комментариев, «мусора» в начале — self-extracting обёртки), ленивое открытие записей, потоковая распаковка в Read, проверка CRC32 по завершении записи. Файлы ≥ 1 МБ отображаются в память (memmap2, только чтение, MAP_PRIVATE); stored-записи (медиа) не копируются — &[u8] из mmap передаётся декодеру. Защита от zip-бомб: лимит коэффициента распаковки 1:200 и суммарного размера 4 ГБ на пакет, лимит записей 100 000, лимит вложенности (OLE внутри ZIP внутри ZIP) — 3 уровня.

Запись: ZipWriter пишет потоково, mimetype первой записью без сжатия (как ODF, библия §3.1), медиа — stored (уже сжаты), XML — deflate уровня 6; все записи с фиксированными временными метками (SOURCE_DATE_EPOCH или время сохранения, округлённое до 2 с) и без «extra»-полей, зависящих от платформы, — одинаковый вход даёт байт-в-байт одинаковый ZIP (воспроизводимость и дифф-дружелюбность в git).

8.2 Безопасное сохранение

SafeWriter::save(path, |w| …):

  1. Временный файл в том же каталоге .<name>.mtmp-<rand>, запись, fsync(file).
  2. Перенос метаданных: права (chmod), владелец при возможности, xattr (macOS com.apple.metadata:*, теги Finder; Linux user.*), ACL Windows (GetNamedSecurityInfo/SetNamedSecurityInfo), время создания.
  3. Атомарная замена: POSIX rename(2); Windows ReplaceFileW (сохраняет ACL и ADS, в том числе Zone.Identifier?) — нет: ReplaceFileW переносит потоки, кроме случаев, когда они явно удаляются; MOTW снимается только пользователем через «Разрешить редактирование» (§10.2); затем fsync(dir) на POSIX.
  4. Если каталог недоступен для создания файлов (Samba с особыми правами, папки с квотой), резервный путь: запись во временный файл в $TMP, затем копирование поверх оригинала через write-through с резервной копией <name>.bak до успешного завершения; пользователю показывается, что сохранение не атомарно.
  5. Для облачных провайдеров (§8.5) — загрузка во временный объект и переименование/commit на стороне API.

8.3 Журнал автосохранения

Каждые 2 с (debounce от последней транзакции, библия §6) в <data>/autosave/<doc-id>/journal.bin дописывается кадр { u32 len, u32 crc32c, u64 seq, u64 ts_ms, CBOR(Vec<Op>) }. Раз в 5 минут или 1 000 кадров — компактация: полный снимок документа в snapshot.mop (обычный пакет MOP) + новый пустой журнал; старые кадры удаляются после успешной записи снимка. Восстановление: читаем snapshot.mop, применяем кадры по порядку seq до первого с неверным CRC или обрезанного (журнал append-only, поэтому повреждён может быть только хвост). Данные автосохранения шифруются (ChaCha20-Poly1305, ключ в хранилище ОС), если документ открыт с паролем или политика организации требует. Метаданные для Hub: <data>/autosave/index.cbor (doc-id → путь, имя, pid, время) — по нему Hub находит «осиротевшие» сессии после сбоя. Подробности формата — algorithms/kernel-core.md §10.

8.4 Блокировки файлов

8.5 Провайдеры хранилища

#[async_trait]
pub trait StorageProvider: Send + Sync {
    fn id(&self) -> ProviderId;
    async fn list(&self, dir: &ItemId, page: Option<PageToken>) -> Result<Listing>;
    async fn stat(&self, item: &ItemId) -> Result<ItemMeta>;              // size, mtime, etag/version, locks
    async fn read(&self, item: &ItemId, range: Option<Range<u64>>) -> Result<Box<dyn AsyncRead + Send>>;
    async fn write(&self, item: &ItemId, body: WriteBody, precondition: Precondition) -> Result<ItemMeta>; // if-match etag
    async fn lock(&self, item: &ItemId, ttl: Duration) -> Result<LockToken>;
    async fn versions(&self, item: &ItemId) -> Result<Vec<VersionMeta>>;
    async fn watch(&self, item: &ItemId) -> Result<BoxStream<ChangeEvent>>;
    fn capabilities(&self) -> Capabilities;                              // locks, versions, ranges, share links
}

Реализации: LocalFs (файловая система, inotify/FSEvents/ReadDirectoryChangesW), WebDav (Nextcloud, ownCloud, SharePoint через WebDAV, WebDAV-locks), MeridianCloud (наш сервер, фаза 6), S3 (on-prem Enterprise), OneDrive/GoogleDrive/YandexDisk через OAuth (фаза 7, в Hub, так как только у него есть сеть). Окна документов работают с провайдерами через Hub-прокси (meridian-ipc), поэтому песочница окна без сети сохраняется.

8.6 Шифрование и подписи

Что Формат Алгоритмы
Пароль на MOP manifest.xml/encryption: на каждую запись AES-256-GCM, ключ записи из мастер-ключа (HKDF), мастер-ключ из пароля Argon2id (m=64 МБ, t=3, p=1); mimetype и manifest.xml открыты AES-GCM, Argon2id, HKDF-SHA256
Пароль на docx/xlsx/pptx ECMA-376 Part 2 + MS-OFFCRYPTO Agile (AES-256-CBC + SHA-512, 100 000 итераций) — чтение и запись; Standard/RC4 — только чтение по спецификации MS
Пароль на ODF ODF 1.3 encryption (AES-256-CBC, PBKDF2/Argon2 в 1.4) — чтение и запись по спецификации OASIS
PDF RC4/AES-128/AES-256 (R6) — чтение и запись по ISO 32000-2
Подпись MOP signatures/sig-N.cose — COSE_Sign1 поверх манифеста хэшей записей (BLAKE3/SHA-256) Ed25519, ECDSA P-256, RSA-PSS; ГОСТ Р 34.10-2012 через плагин CryptoPro CSP (Enterprise, 152-ФЗ)
Подпись OOXML/ODF XML-DSig / XAdES-B(-T) в _xmlsignatures/ и META-INF/documentsignatures.xml как в Office/LibreOffice
Ключи и сертификаты хранилища ОС (CNG, Keychain, PKCS#11 на Linux), смарт-карты —

Криптография — только проверенные крейты (ring/aws-lc-rs, aes-gcm, argon2, ed25519-dalek), без собственных примитивов; все проверки подписей — в ядре, UI показывает результат (SignatureStatus).


9. Скрипты и расширения — MeridianScript

9.1 Объектная модель

Единое пространство meridian (TypeScript-типы генерируются из Rust-деклараций tools/ts-types):

declare namespace meridian {
  const app: Application;                          // текущее приложение (write | sheets | …)
  const documents: DocumentCollection;             // открытые документы этого окна (+ Hub: все)
  interface Application { name: AppName; version: string; edition: Edition; locale: string;
    commands: CommandRegistry; ui: UiHost; clipboard: Clipboard; events: EventHub; storage: ScriptStorage; }
  interface Document { id: string; title: string; path?: string; kind: DocKind; modified: boolean;
    save(opts?: SaveOptions): Promise<void>; export(format: FormatId, opts?): Promise<Blob>;
    undo(): void; redo(): void; transaction<T>(name: string, f: () => T): T;   // одна группа undo
    on(event: DocEvent, h: Handler): Disposable; }
  // Write
  interface TextDocument extends Document { body: Range; sections: Section[]; styles: StyleSheet;
    fields: Field[]; comments: Comment[]; find(q: FindQuery): Range[]; }
  interface Range { text: string; start: Position; end: Position; paragraphs: Paragraph[];
    font: Font; paragraphFormat: ParagraphFormat; insert(text: string, where?: 'before'|'after'|'replace'): Range;
    tables: Table[]; shapes: Shape[]; select(): void; }
  // Sheets
  interface Workbook extends Document { sheets: Sheet[]; activeSheet: Sheet; names: NamedRanges; calculate(): void; }
  interface Sheet { name: string; range(a1: string): CellRange; cell(r: number, c: number): Cell; usedRange: CellRange;
    rows: RowCollection; columns: ColumnCollection; charts: Chart[]; pivots: PivotTable[]; }
  interface CellRange { values: CellValue[][]; formulas: string[][]; format: CellFormat; merge(): void;
    sort(opts): void; autofill(dest: CellRange): void; }
  // Slides / Draw / Paint / Notes / Mail / Base / Forms — аналогично: Presentation, Slide, Shape, Image, Layer,
  // Notebook, Page, MailStore, Message, Database, Table, Query, Form, Question …
}

Правила API: все мутации идут через Transaction (один вызов скрипта = одна группа undo, если не обёрнут в transaction()); объекты — тонкие прокси над NodeId/CellAddr, живут не дольше документа; коллекции — итерируемые, с ленивыми снимками; долгие операции — Promise, выполнение скрипта не блокирует UI (QuickJS в отдельном потоке, вызовы в модель — через очередь команд ядра с батчингом).

9.2 События

document.on('change' | 'beforeSave' | 'afterSave' | 'selectionChange' | 'calculate' | 'open' | 'close', h), app.events.on('command:<id>' | 'documentOpened' | 'themeChanged' | 'idle', h); обработчики beforeSave могут отменить (event.preventDefault()) с причиной. Событие несёт Transaction-дифф в терминах API (changes: Change[]). Бюджет обработчика — 50 мс, превышение → предупреждение в консоль, 3 превышения → обработчик отключается до перезапуска.

9.3 UI-расширения: манифест

{
  "id": "ru.example.citations", "version": "1.4.0", "name": { "ru": "Цитаты", "en": "Citations" },
  "apiVersion": "1", "apps": ["write"], "minProductVersion": "1.0.0",
  "main": "main.js", "types": "types.d.ts",
  "permissions": ["document.read", "document.write", "network:api.crossref.org", "storage.local"],
  "contributes": {
    "commands": [{ "id": "citations.insert", "title": { "ru": "Вставить цитату" }, "icon": "icons/cite.svg", "shortcut": "Ctrl+Alt+C" }],
    "ribbon": [{ "tab": "references", "group": { "id": "citations", "title": { "ru": "Цитаты" } }, "items": ["citations.insert"] }],
    "panels": [{ "id": "citations.library", "title": { "ru": "Библиотека" }, "side": "right", "view": "panel.html" }],
    "contextMenu": [{ "when": "selection.isText", "items": ["citations.insert"] }],
    "fileHandlers": [{ "ext": ".bib", "command": "citations.importBib" }],
    "settings": [{ "key": "style", "type": "enum", "values": ["gost", "apa", "mla"], "default": "gost" }]
  },
  "activation": ["onCommand:citations.insert", "onDocumentKind:write"],
  "author": { "name": "Example", "url": "https://example.ru" }, "license": "MIT"
}

Панели расширений — HTML/QML? Решение: QML-декларации в ограниченном подмножестве (panel.qml из белого списка компонентов Meridian.Ui: списки, формы, кнопки, веб-вью нет) либо декларативная JSON-форма ("view": { "type": "form", "fields": [...] }). Веб-вью не даём: тянет Chromium/WebEngine (+150 МБ) и ломает модель безопасности; сложные интерфейсы — через JSON-формы и команды.

9.4 Права

Право Что даёт Запрос
document.read / document.write чтение/изменение текущего документа при установке
documents.all все открытые документы при установке, с предупреждением
files.read / files.write диалог выбора файла; доступ только к выбранному (как в порталах) у пользователя в момент вызова
files.path:<glob> доступ без диалога к папке Enterprise-политикой или явным согласием в настройках
network:<host> fetch к перечисленным хостам через Hub-прокси при установке, список хостов виден
clipboard.read / clipboard.write буфер обмена write — при установке; read — у пользователя при первом вызове
ui.notifications, ui.dialogs уведомления, диалоги при установке
storage.local KV-хранилище расширения (≤ 50 МБ) без запроса
shell.open открыть URL во внешнем браузере у пользователя при вызове

Без права вызов API бросает PermissionDenied; права по умолчанию Free-редакции ограничены (скрипты из магазина — Home+, библия §9.1). Enterprise-политика может запретить любые права и разрешить только подписанные организацией расширения.

9.5 Песочница QuickJS

9.6 Запись макросов

Все действия пользователя идут через CommandBus (команда + аргументы в терминах API). Рекордер подписывается на шину и генерирует код: doc.selection.font.bold = true вместо низкоуровневых Op. Команды без «API-эквивалента» (перетаскивание фигуры) записываются как shape.move({x, y}). Выделение записывается относительным («текущее выделение»), ссылки на ячейки — абсолютными или относительными по переключателю (как в Excel).

9.7 Транслятор VBA → MeridianScript

Переводим: Sub/Function/End, Dim/Const/ReDim, типы (→ TS-типы), If/ElseIf/Select Case, For/For Each/Do/While, With, Exit, On Error Resume Next/GoTo 0 (→ try/catch с флагом), операторы (&, Mod, \, Like → regex), строки/даты/математика (Len, Mid, Left, Right, InStr, Replace, Format, DateAdd, Now, Round, Int, … → helper- библиотека vba-compat), объекты Excel (Workbooks, Worksheets, Range, Cells, Selection, ActiveSheet, ActiveCell, Application.ScreenUpdating/Calculation), Word (Documents, Selection, Range, Paragraphs, Tables, Find), MsgBox/InputBox → app.ui.dialogs. Помечаем // TODO(vba): unsupported с исходной строкой: Declare (Win32 API), ActiveX/COM CreateObject, UserForm, DDE*, Shell, SendKeys, события Auto_Open (→ требуют разрешения пользователя), Variant-трюки с Empty/Nothing (переводятся частично). Результат — редактируемый TS с комментариями; документ .docm/.xlsm сохраняет исходный VBA (vbaProject.bin) для round-trip.

9.8 Python-плагин

Встроенный CPython 3.12 (python-build-standalone) в процессе meridian-python-host с тем же IPC и песочницей, что у кодеков, плюс права из манифеста; API meridian доступен как модуль-прокси (генерируется из тех же деклараций, что .d.ts); pip отключён, зависимости — только из пакета .mxt (чистый Python или колёса для нашей сборки). Плагин опционален (отдельная загрузка ~30 МБ).

9.9 Пакет расширения .mxt

ZIP: manifest.json, main.js (+ модули), types.d.ts, locales/<lang>.json, assets/, python/ (опц.), LICENSE, signature.cose — COSE_Sign1 (Ed25519) разработчика поверх списка хэшей файлов; магазин добавляет контрподпись (signature.store.cose) после проверки (автоматический анализ прав, ручная модерация для network/files.path). Установка: из магазина (Hub скачивает, проверяет две подписи), из файла (предупреждение «непроверенный источник», в Enterprise — только по белому списку издателей). Обновления — через магазин, semver, автообновление по умолчанию для patch.

9.10 Почему нет совместимости с плагинами LibreOffice/Office

LibreOffice-расширения (.oxt) привязаны к UNO — объектной модели с тысячами интерфейсов и реализацией на C++/Java/Python внутри процесса LibreOffice; эмулировать UNO означало бы воспроизводить его внутреннюю модель документа. Office VSTO/COM-надстройки — нативный код Windows, COM-интерфейсы Word/Excel, in-process, без песочницы: несовместимо с кроссплатформенностью и безопасностью по умолчанию. Office.js (веб-надстройки) ближе всего, но требует встроенного браузера и работает с другой моделью (Office.context); вместо этого даём переносчик: документация соответствия Office.js → meridian.* и помощник миграции в редакторе скриптов. VBA — единственная «совместимость», и та через транслятор (§9.7).


10. Безопасность

10.1 Модель угроз

Угроза Вектор Защита
Вредоносный документ (RCE через парсер) искажённый ZIP/XML/CFB/PDF/изображение Rust без unsafe в кодеках, песочница codec-host (§7.2), лимиты, фаззинг (§10.8)
Макро-вирусы скрипты в документе не запускаются без явного разрешения (библия §3.1); песочница QuickJS; права по манифесту; подписи
Фишинг через поля и ссылки HYPERLINK, INCLUDEPICTURE, INCLUDETEXT, ссылки на file://, \\server\share внешние ресурсы не загружаются без согласия; ссылки на нестандартные схемы и UNC — предупреждение; показ реального URL при наведении
DDE/OLE-подобные вещи DDEAUTO, OLE-объекты с автозапуском, встроенные EXE DDE не поддерживается (поле отображается как текст с предупреждением); OLE — только отображение превью, активация запрещена; вложения открываются через системное приложение после подтверждения
Внешние ресурсы (tracking) удалённые картинки, шрифты, шаблоны attachedTemplate, XML-карты запрет по умолчанию, запрос с показом хоста, политика Enterprise
Утечка через метаданные авторы, пути, правки, скрытый текст «Инспектор документа» перед отправкой/публикацией; очистка в экспорте PDF по умолчанию
Подмена обновлений MITM, скомпрометированный CDN подпись манифестов (Ed25519) + TUF-роли, pinned-ключи в сборке, защита от отката версии
Кража токенов/лицензий файлы настроек, дампы токены только в хранилищах ОС; лицензия подписана и привязана к устройству; секреты не логируются
Расширение с избыточными правами магазин, side-load манифест прав, модерация, подписи, политика Enterprise
Шифрование документов слабыми алгоритмами старые RC4/XOR чтение с предупреждением «устаревшая защита», запись только современных схем

10.2 Защищённый просмотр

Файл считается «из интернета», если несёт метку: Windows — ADS Zone.Identifier с ZoneId=3/4 (MOTW); macOS — xattr com.apple.quarantine; Linux — xattr user.xdg.origin.url/user.xdg.referrer.url или путь внутри ~/Downloads. Такие файлы, а также вложения почты и файлы с сетевых дисков из «ненадёжных зон» (политика), открываются в защищённом просмотре: разбор и раскладка в песочнице с рендером в тайлы, в окне — только просмотр, печать и копирование текста; скрипты, поля с внешними ресурсами, OLE и ссылки неактивны. Кнопка «Разрешить редактирование» снимает метку (только после подтверждения) и открывает документ обычным путём. Политика Enterprise может запретить снятие метки для определённых зон.

10.3 Уровни доверия скриптов

Уровень Кто Что можно
0 — недоверенный скрипт в документе без подписи ничего (показывается уведомление и кнопка «разрешить один раз»)
1 — разрешено пользователем явное «разрешить» для документа права из манифеста документа, но без files.path, network вне белого списка
2 — подписанный издатель расширение из магазина или подпись из белого списка организации права манифеста
3 — политика организации Enterprise MDM/GPO всё, что разрешено политикой, без запросов

Решения хранятся в <data>/trust.db (SQLite) с хэшем скрипта: изменение скрипта сбрасывает доверие.

10.4 Хранение секретов

Токены Meridian ID, пароли WebDAV, ключи API расширений — только через trait SecretStore (meridian-id): Keychain (macOS, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), DPAPI + Credential Manager (Windows), Secret Service/libsecret (Linux; при отсутствии — зашифрованный файл с ключом от keyctl/пароля сессии и предупреждением). В логах — редактирование по ключам (token, password, Authorization).

10.5 Обновления с подписью

Манифест update.json подписан Ed25519 (ключ targets), срок действия timestamp-роли 7 дней (защита от «заморозки»), snapshot перечисляет версии (защита от отката), root ротация ключей — через подписанный новым и старым ключами root.json. Пакеты — хэш BLAKE3 в манифесте, проверка до применения; дельты применяются к проверенной базе и результат сверяется с хэшем полной версии.

10.6 Телеметрия и отчёты о сбоях

Только по согласию (библия §9.4): отдельные переключатели «анонимная статистика» и «отчёты о сбоях», видны в первом запуске и в настройках. Отчёт о сбое — minidump (crashpad) + версия/ОС/GPU + последние 200 строк лога без путей и текста документов; перед отправкой — предпросмотр содержимого. Метрики — счётчики использования команд и времени операций, без идентификаторов пользователя (случайный ID установки, сбрасывается по кнопке).

10.7 Фаззинг и санитайзеры в CI

10.8 SBOM и политика зависимостей


11. Производительность

11.1 Бюджеты

Метрика Бюджет Как измеряем Где проверяется
Запуск Hub до интерактивности < 1,0 с (SSD, тёплый кэш ОС) tracing span startup → первый кадр benches/startup, CI ночной
Запуск Write с пустым документом < 1,5 с то же то же
Открытие docx 100 стр. (текст + таблицы) < 1,0 с до первой страницы, < 3 с до полной раскладки span open / layout.complete корпус perf/
Ввод символа → обновление экрана < 16 мс (p99 < 32 мс) span edit.keystroke до presentation UI-тест с автоматическим вводом
Прокрутка 60 к/с на 4K при 200 % (p99 кадра < 16,6 мс) кадровый тайминг QRhi ночной UI-бенч
Пересчёт 1 М ячеек (SUM-цепочки, 10 % формул) < 1,0 с полный, < 50 мс инкрементальный на 1 изменение benches/calc CI на каждый PR (порог регрессии 10 %)
Память пустого окна документа < 150 МБ RSS ps/GetProcessMemoryInfo после 5 с простоя ночной
Память docx 100 стр. с 50 картинками < 400 МБ то же корпус perf/
Сохранение MOP 100 стр. < 500 мс span save корпус
Экспорт PDF 100 стр. < 2 с span export.pdf корпус
Полнотекстовый поиск по 10 000 документов (Hub) < 100 мс tantivy-бенч ночной

11.2 Как измеряем

11.3 Стратегии


12. Платформы

12.1 Windows (10 1809+ / 11, x64 и arm64)

Область Решение
Компилятор MSVC 2022 (x86_64-pc-windows-msvc, aarch64-pc-windows-msvc), Rust через -C target-feature=+crt-static? нет — динамический CRT, общий с Qt
Шрифты DirectWrite для перечисления и системных метрик; шейпинг — всегда HarfBuzz (rustybuzz) для детерминизма; растеризация Skia
GPU D3D12 через Skia; резерв ANGLE (GL ES поверх D3D11), затем CPU
Окно CSD (собственный заголовок, библия §5.3) через DwmExtendFrameIntoClientArea + snap layouts Win11 (WM_NCHITTEST для кнопки «развернуть»)
Установка MSIX (Store и sideload, AppInstaller) и MSI (WiX 4) для корпоративного развёртывания; оба подписаны EV-сертификатом; MSI поддерживает /quiet, свойства EDITION, LICENSE_KEY, UPDATE_CHANNEL, DISABLE_TELEMETRY для GPO
Ассоциации .mwx/.msx/… и чужие форматы — через Default Programs с ProgId Meridian.Write.1; не захватываем чужие расширения без согласия при установке
Explorer Preview Handler (COM, IPreviewHandler, out-of-process prevhost.exe) — рендер первых страниц через meridian-convert в песочнице; миниатюры — IThumbnailProvider из thumbnail.png пакета MOP (без парсинга)
Поиск Windows IFilter (COM) — извлечение текста/свойств для индекса; реализован на meridian-codecs в режиме «только текст»
Печать XPS Print API / GDI (§6.7), диалог — QPrintDialog (нативный)
Хранилище секретов DPAPI + Credential Manager
Политики ADMX-шаблоны для GPO (packaging/windows/policies/): запрет макросов, зоны, прокси обновлений, принудительный канал

12.2 macOS (13 Ventura+, Apple Silicon и Intel)

Область Решение
Компилятор clang из Xcode 15+, aarch64-apple-darwin + x86_64-apple-darwin → lipo universal binary; Qt universal
Шрифты CoreText для перечисления/системных шрифтов; шейпинг HarfBuzz; Apple Color Emoji через sbix
GPU Metal через Skia (GrMtlBackendContext из QRhi Metal); резерв CPU
Окно нативный заголовок (NSWindow с titlebarAppearsTransparent, toolbar-стиль unified), меню — нативное NSMenu (обязательно для macOS-привычек), ribbon — ниже заголовка
Установка DMG с подписанным и нотаризованным .app (Developer ID, hardened runtime, notarytool); отдельная сборка для Mac App Store с App Sandbox (см. вопросы к библии: ограничения на процессную модель — помощники как XPC-сервисы)
Quick Look расширение QLPreviewExtension (app extension, sandboxed): рендер через meridian-convert-библиотеку; миниатюры QLThumbnailProvider из thumbnail.png
Spotlight CSImportExtension (Core Spotlight importer) — текст и метаданные MOP/OOXML/ODF
Печать PDF → NSPrintOperation; диалог нативный
Хранилище Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
Интеграция Services меню (открыть выделенное в Write), Handoff не нужен, Continuity Camera для Notes/Paint, NSDocument-подобное поведение (версии — свои), Dark Mode по NSAppearance

12.3 Linux (glibc 2.31+, x64 и arm64)

Область Решение
Компилятор GCC 12+/Clang 15+, сборка в контейнере на базе Ubuntu 22.04 (старый glibc для совместимости)
Шрифты fontconfig для перечисления и настроек (hinting/rgba); шейпинг HarfBuzz; Noto Color Emoji (CBDT)
GPU Vulkan через Skia (QRhi Vulkan); резерв OpenGL 3.3 / GLES 3.0; CPU
Окно Wayland (xdg-decoration где есть; CSD через собственный заголовок, библия §5.3), X11 — CSD с тенями через _GTK_FRAME_EXTENTS; системная тема — чтение org.freedesktop.appearance (portal) для светлой/тёмной
Установка Flatpak (Flathub, главный канал; порталы для файлов FileChooser, печати Print, уведомлений, запуска ссылок — без прямого доступа к $HOME), AppImage (переносимый), deb/rpm (репозитории с подписью GPG для Enterprise), Snap (по запросу)
Интеграция DE .desktop-файлы с MimeType и действиями («Новый документ»), иконки hicolor, shared-mime-info XML для MOP, миниатюры через thumbnailer (.thumbnailer файлы) с meridian-convert, GNOME Nautilus/KDE Dolphin — предпросмотр через стандартные механизмы, KDE Baloo/Tracker — meridian-convert --text как экстрактор
Печать CUPS (PDF напрямую), диалог QPrintDialog/portal
Хранилище Secret Service (gnome-keyring, KWallet через API), резерв — зашифрованный файл
IME Fcitx5/IBus через Qt (QT_IM_MODULE), Wayland text-input-v3

12.4 Общее для всех платформ


13. Сборка, CI/CD, тестирование

13.1 Toolchain

Инструмент Версия Примечание
Rust stable, закреплён 1.97.1 в CI; MSRV 1.85 (edition 2024) rust-toolchain.toml → stable для разработчиков
Qt 6.8.2 LTS (коммерческая или LGPLv3 — см. вопросы к библии) модули: Core, Gui, Qml, Quick, QuickControls2, Widgets, PrintSupport, Svg, ShaderTools
CMake / Ninja ≥ 3.24 / ≥ 1.11
C++ C++20: MSVC 19.40+, clang 15+, GCC 12+
Skia skia-safe закреплённой версии (m130+), сборка из бинарей skia-binaries или из исходников в CI фичи: gl, vulkan, metal, d3d, textlayout выключен (свой шейпинг)
QuickJS-ng через rquickjs с закреплённой версией
Yrs yrs 0.21+
Python 3.12 для скриптов репозитория
just ≥ 1.25 рецепты в justfile

13.2 Рецепты just

setup (проверка окружения), build / build-kernel / build-shell, check, test, fmt, lint (fmt-check + clippy + deny), corpus, bench, fuzz <target>, doc, package-win|mac|linux, docs-site, clean. Переменные: MERIDIAN_PROFILE=dev|release, QT_DIR.

13.3 Матрица CI (.github/workflows/ci.yml)

Job Раннеры Шаги Когда
lint ubuntu-22.04 rustfmt, clippy -D warnings, cargo-deny каждый PR/push
kernel ubuntu-22.04, windows-2022, macos-14 (arm64 + x64 cross) build, test, doc, компиляция meridian.h каждый PR/push
shell те же + Qt через install-qt-action cmake configure/build, ctest по флагу BUILD_SHELL/workflow_dispatch до стабилизации оболочки, затем всегда
corpus ubuntu-22.04 (self-hosted для полного корпуса) scripts/corpus-test.sh --report вручную / ночью; PR — поднабор smoke (200 файлов)
nightly все ОС санитайзеры, Miri, фаззинг 1 ч/цель, бенчмарки с историей, память ночью
release все ОС пакеты, подпись, нотаризация, манифест обновлений, SBOM, публикация в канал теги v*
services ubuntu образы сервисов, миграции, интеграционные тесты с PostgreSQL изменения в services/

Кэширование: Swatinem/rust-cache (ключ по ОС и Cargo.lock), sccache для C++/Skia на self-hosted раннерах, кэш Qt (install-qt-action cache), LFS-кэш корпуса на self-hosted. Время цели: lint ≤ 5 мин, kernel ≤ 15 мин, shell ≤ 25 мин.

13.4 Статический анализ

cargo fmt, clippy (all + выбранные pedantic: must_use_candidate, needless_pass_by_value, cast_lossless, …), cargo-deny (лицензии, advisories, bans, sources), cargo-udeps (ночью), clang-tidy (modernize-*, bugprone-*, cppcoreguidelines-* выборочно), qmllint для QML, semgrep (свои правила слоёв и безопасности), scripts/check-layers.py, scripts/check-docs.py.

13.5 Тесты

Уровень Что Инструмент Запуск
Unit каждый крейт, свойства инвариантов (proptest) cargo test PR
Снимочные корпус → PNG + .dl.txt, порог 0,2 % пикселей (ΔE > 6 после размытия 1 px), для .dl.txt — точное совпадение scripts/corpus-test.sh smoke на PR, полный ночью
Round-trip открыть → сохранить → открыть → модель равна (структурный дифф), для чужих форматов — сохранение неизвестных элементов tests/integration PR
Фаззинг парсеры, непрерывно cargo-fuzz, ClusterFuzzLite ночью + постоянно
UI сценарии: создать документ, ввести, отформатировать, сохранить, открыть диалоги; a11y-дерево Qt Test (QTest), Squish ночью; smoke на PR
Бенчмарки micro + macro criterion, tools/convert --bench PR (micro, порог 10 %), ночью (macro)
Совместимость открытие наших файлов в Word/LibreOffice (round-trip через их конвертеры на self-hosted раннере) tools/corpus-ref еженедельно

13.6 Версионирование и ветки


14. Интеграция лицензирования в архитектуру

Детали ключей, активации, сервера и портала — 10-licensing-accounts.md; здесь — только то, как лицензия живёт в коде.


15. Риски и альтернативы

15.1 Оболочка: Qt против Slint и Tauri

Оценки 1–5 (5 — лучше), веса по важности для десктопного офисного пакета.

Критерий (вес) Qt 6 / QML Slint Tauri (веб-UI)
Зрелость, крупные приложения (5) 5 — Krita, WPS, Telegram 2 — молодой 3 — много небольших
IME, сложный ввод текста (5) 5 3 4 (браузер), но холст — свой
Доступность (a11y) на 3 ОС (4) 4 2 4 (DOM), холст — ручная работа
Печать и системные диалоги (4) 5 2 3
Интеграция с Rust (4) 3 — cxx-qt, C++ прослойка 5 — нативно Rust 4 — IPC JSON
Производительность UI, память (4) 4 5 2 — WebView + Node-подобный мост
Собственный стиль, темы (3) 4 4 5
Wayland/HiDPI/дробные масштабы (3) 5 4 3 (зависит от WebView ОС)
Лицензия и стоимость (3) 3 — LGPLv3/коммерческая 4 — GPLv3/Royalty-free/коммерческая 5 — MIT
Найм разработчиков (2) 4 2 5
Взвешенная сумма (макс. 185) 158 119 131

Qt остаётся решением (библия §4). Slint — наблюдаем: когда появятся IME-паритет, a11y на уровне Qt и печать, переход возможен благодаря тому, что вся логика — в Rust, а QML-слой тонкий (§15.2).

15.2 Если cxx-qt станет узким местом

Риски: скорость генерации кода, ограничения на типы (нет generics в мосте), медленные изменения проекта, оверхед на каждом вызове QObject ↔︎ Rust. План:

  1. Держать мост тонким с первого дня: QObject-модели только для того, что видит QML (списки, свойства, команды); всё остальное — через C ABI meridian-ffi с cbindgen-заголовком и батчингом (ChangeSet раз в кадр).
  2. Критерий отказа: > 20 % времени кадра в мосте на бенче «ввод текста» или блокирующие баги без фикса > 3 мес.
  3. Запасной путь: ручные QObject-обёртки на C++ поверх C ABI (как сейчас у meridian_document_*) — больше кода, но полный контроль; генерация обёрток собственным скриптом из тех же деклараций, что .d.ts (tools/ts-types расширяется до C++).
  4. Крайний случай: холст и большая часть панелей рендерятся самим ядром (Skia) внутри одного QQuickItem, QML остаётся для ribbon/диалогов — «Krita-подход».

15.3 Стратегия отказа от Skia в пользу vello

Skia — большой C++-проект (сборка 20+ мин, ~30 МБ бинарей, зависимости от Clang/Ninja/GN); vello (wgpu) — Rust, компактный, GPU-compute растеризация, но без зрелого текста, PDF-бэкенда и CPU-резерва.

15.4 Прочие риски

Риск Вероятность Влияние Митигация
Точность импорта OOXML ниже цели («не хуже оригинала») высокая критическое корпус 10 000+, приоритет P0 на фазу 1, еженедельный отчёт по ΔE, шрифты-метрические двойники
Производительность CRDT на больших документах средняя высокое схема «база + дельты» (§5.2), GC истории, бенчи в CI
Лицензия Qt (LGPLv3 vs коммерческая) и статическая линковка средняя высокое динамическая линковка под LGPL, юридическая проверка до фазы 1 (см. вопросы к библии)
Размер дистрибутива (Qt + Skia + шрифты + CPython) высокая среднее ленивые модули (Python, шрифты письменностей, словари) как скачиваемые компоненты; целевой базовый пакет ≤ 250 МБ
Песочницы ОС ломаются при обновлениях (macOS sandbox-exec deprecated) средняя среднее XPC-сервисы для macOS с фазы 1, тесты песочниц в CI на реальных раннерах
Одна команда на 13 приложений — расфокус высокая высокое фазы по библии §7, общие модули раньше приложений, Publisher/Plan — фаза 7

Вопросы к библии

  1. Лицензия исходного кода. Библия описывает коммерческий продукт, но не говорит, открыт ли код (open core / проприетарный / source-available). От этого зависят license в Cargo.toml, политика зависимостей (§10.8) и выбор LGPLv3 против коммерческой лицензии Qt. Предложение: ядро и кодеки — открыты (Apache-2.0), оболочка, Assist, сервисы — проприетарные; решение нужно до фазы 1.
  2. Qt: LGPLv3 или коммерческая. LGPLv3 требует динамической линковки и возможности замены Qt пользователем, что усложняет App Store-сборку и MSIX; коммерческая лицензия — ежегодная плата за разработчика. Нужно решение владельца с оценкой бюджета.
  3. App Store для macOS и Microsoft Store. App Sandbox ограничивает процессную модель (§7): помощники должны быть XPC-сервисами внутри бандла, без fork/exec произвольных процессов; Store-версия, вероятно, будет без Python-плагина и с ограниченными портами. Подтвердить, нужны ли Store-сборки в 1.0 или только прямые.
  4. Минимальные версии ОС. Библия: Windows 10/11, macOS 13+, Linux без уточнения. Предложение зафиксировать: Windows 10 1809+ (D3D12 feature level 11, MSIX), glibc 2.31+ (Ubuntu 20.04+), Wayland и X11.
  5. Единицы в MOP. Библия говорит о «XML по духу OOXML/ODF». Предложение: в MOP-XML писать длины в EMU целыми (как DrawingML) с необязательным суффиксом для читаемости в плоском варианте ("12700emu"), а не в десятичных cm — это сохраняет детерминизм §3.1 и обратимость. Утвердить в spec/mop.
  6. Paint и совместная работа. Библия относит CRDT к «Meridian Sync» без перечисления приложений; архитектура исключает пиксели Paint из CRDT (§5.1). Подтвердить, что совместная работа нужна для Write/Sheets/Slides/Draw/ Notes/Forms, а Paint/PDF/Mail/Base — вне её.
  7. Веб-панели расширений. Библия обещает «расширяемость — часть продукта», архитектура запрещает WebView в расширениях (§9.3) ради безопасности и размера. Если магазину нужны богатые панели, потребуется решение о встроенном WebEngine (+150 МБ) — предлагается отложить до фазы 7.
  8. Meridian Print как отдельный модуль. Библия перечисляет Print среди модулей ядра; здесь это тонкий крейт meridian-print над meridian-render + ОС. Если ожидается больший объём (управление очередями, профили принтеров, цветокоррекция), выделить задачу в 08-roadmap.md.
  9. Телеметрия по умолчанию в Trial. Для оценки конверсии триала полезны метрики, но библия требует согласия. Оставляем «только по согласию» и спрашиваем в первом запуске; подтвердить.
  10. Нумерация документов. Файл 03-architecture.md ссылается на 10-licensing-accounts.md и 08-roadmap.md (ещё не написаны) и на docs/algorithms/; имена закрепить в 08-roadmap.md/оглавлении docs.