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 Принципы
- Ядро без UI и без сети. Ядро — библиотека чистых
структур данных и алгоритмов. Любой ввод — вызов функции, любой вывод —
значение или событие в очереди. Это даёт тестируемость (100 % логики —
cargo test), компиляцию в WebAssembly и возможность запускать ядро в песочнице без прав. - Одна модель — много представлений.
meridian-textобслуживает абзацы Write, текстовые блоки Slides, заметки Notes, поля форм Base;meridian-grid— листы Sheets, таблицы Write, таблицы данных Chart;meridian-scene— фигуры Draw, слайды, холст Notes, обтекаемые объекты Write. Представление (страница, слайд, лист, холст) — это раскладчик поверх общей модели, а не отдельная модель. - Песочницы по умолчанию. Чужие файлы разбираются в отдельном процессе с урезанными правами (§7), скрипты — в QuickJS с лимитами (§9), внешние ресурсы документов не загружаются без разрешения (§10).
- Детерминизм раскладки. Один и тот же документ с
одним набором шрифтов даёт байт-в-байт одинаковый список отображения на
любой машине и ОС. Это условие для совместной работы (все участники
видят одну нумерацию страниц), снимочных тестов корпуса и
воспроизводимых отчётов об ошибках. Средства: целые EMU (§3),
целочисленный шейпинг, шрифты в комплекте, фиксированный порядок обхода,
BTreeMap/детерминированные хэши, запретf32-накоплений в раскладке (docs/algorithms/kernel-core.md§19). - Каждое изменение — транзакция. Модель меняется
только через
Transactionс наборомOp; из транзакций строятся undo, автосохранение, CRDT и история версий (§4.4, §5). - Чужой файл важнее нашего кода. Неизвестные элементы форматов сохраняются и возвращаются при записи (round-trip), а не теряются; падение кодека — не падение документа.
- Границы — явные. 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*
Правила репозитория:
- Крейты именуются
meridian-<domain>(ядро/платформа) иcodec-<format>(кодеки); имена библиотек — с подчёркиванием (meridian_core). Версии всех крейтов едины (workspace.package.version). apps/<app>/не содержит кода оболочки: это декларативная конфигурация (ribbon, команды, модули), которую общая оболочка читает при запуске; разница между Write и Slides — в конфигурации и в том, какие крейты-раскладчики подключены, а не в копиях UI.- Документы и спецификации живут рядом с кодом и проверяются в CI
(
scripts/check-docs.py: ссылки, соответствие таблиц расширений библии иapps/*/app.toml).
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 |
| тело письма (HTML-подмножество) | Document Write-типа |
Flow без страниц |
TextBody = Vec<Paragraph> + локальные
стили; Paragraph = PieceTable +
AttrSpans + абзацные Attrs. Движок один,
различается LayoutHost — трейт, через который раскладчик
спрашивает доступную ширину, обтекаемые объекты и позицию разрыва
страницы/колонки.
4.4 Идентификаторы и адресация
- Стабильный ID:
NodeId(UUIDv7) присваивается при создании и переживает все перемещения; при импорте чужого формата ID выводятся детерминированно (uuid_v7_from_parts(hash(path), 0, hash(content))), чтобы повторный импорт одного файла давал те же ID (важно для сравнения версий). - Путь:
NodePath = Vec<(NodeId, u32)>— цепочка от корня с индексом ребёнка; используется для человекочитаемых ссылок (body/3/para/12) и для быстрого спуска без хэш-поиска. - Текстовая позиция:
TextPos { node: NodeId, byte: u32 }— байтовое смещение UTF-8 внутри абзаца на границе символа; для CRDT —StickyIndexYrs ((client, clock)элемента + ассоциация слева/справа). - Ячейка:
CellAddr { sheet: SheetId, row: u32, col: u32 }(0-базовые), диапазон —Range { a, b }. - Фигура:
NodeId; якорь в тексте —Anchor { para: NodeId, byte: u32, mode: Inline | Floating(...) }.
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. Решение —
базовый снимок + дельты:
- При открытии файла (импорт xlsx или MOP без истории) данные листа
загружаются в
meridian-grid(колоночные арены, ~16 Б/ячейка) и получаютbase_hash(BLAKE3 колонок). - В
yrs::Docхранятся только ячейки, изменённые после базы:cells: Map<"s:r:c", CellData>; удалённые — значениеNull. Чтение ячейки: сначала дельта, потом база. - Структурные операции (вставка строки) записываются как операции над
картой
structureс LWW-семантикой (Arrayиз{op, index, count, ts}) и применяются к базе детерминированно всеми участниками в порядке(ts, client). Конфликт двух одновременных вставок строк решается этим порядком — обе вставки применяются, адреса в дельтах переписываются черезRowMap(сжатая таблица сдвигов, O(log n) на запрос). - Компактация: когда дельта > 20 % базы или по команде «сохранить»,
дельта вливается в базу, пишется новый
base, в CRDT вставляется маркерbase_hash, и записи дельты до маркера удаляются (Map::remove), что после GC Yrs освобождает память. Участники с другимbase_hashдокачивают новую базу у сервера/файла.
Эта схема держит Yrs-структуру пропорциональной числу правок, а не размеру таблицы; совместная работа над 1 М-ячеек листом стоит столько же, сколько над пустым.
5.3 Локальная отмена поверх CRDT
UndoManager Yrs отслеживает только операции с
Origin::Local (tracked_origins = {LOCAL}),
поэтому «Отменить» никогда не откатывает чужие правки. Группировка:
- временной порог
capture_timeout = 500 мсмежду нажатиями → одна группа (набор слова); - явные границы: каждая команда (
CommandId) закрывает группу (undo_manager.stop()), кроме команд с флагомmerges_with_previous(ввод символа, Backspace, перетаскивание); Composite-транзакции всегда одна группа;- при получении удалённого обновления, пересекающегося с верхней группой, группа закрывается (как в Yjs).
Для данных вне 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 История версий
Version { id: Uuid, name, author, created_at, snapshot: yrs::Snapshot, parent: Option<Uuid> };yrs::Snapshot= state vector + delete set, занимает O(число клиентов) байт.- Автоматические версии: при сохранении и каждые 30 мин активного редактирования; именованные — по команде.
- Просмотр версии:
Doc::snapshot_to_docстроит состояние на момент снимка (требуетgc = falseдо этой версии); сравнение двух версий — диф по дереву узлов и по тексту (algorithms/kernel-core.md§5). - Сборка мусора: удалённые элементы старше 30 дней и не нужные ни
одной сохранённой версии удаляются (
Docсgc = trueпосле «сжать историю»); режим «полная история» — настройка документа для аудита.
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)
- Транспорт: WebSocket (TLS), бинарные кадры; первое сообщение
Auth { token, doc_id, client_caps }. - Сообщения (CBOR-заголовок + lib0-payload):
SyncStep1 { sv },SyncStep2 { update },Update { update, seq },Ack { seq },Awareness { states },Error { code, msg },Rebase { base_hash }(Sheets),Permission { level }. - Права:
owner | editor | commenter | viewer; сервер декодирует каждоеUpdateи проверяет затронутые корневые типы:commenterможет писать только вcommentsиawareness,viewer— толькоawareness; нарушение →Error(403)и разрыв. Проверка — на уровне структур Yrs (update.decode().iter_items()), без выполнения логики документа. - Персистентность: PostgreSQL
(
doc_updates (doc_id, seq, bytes)) + периодические снимки в S3; при подключении клиент получает снимок + хвост обновлений. - Офлайн: клиент копит обновления в
sync_outbox(SQLite,meridian-sync), при соединении обменивается state vector'ами; конфликтов нет по построению CRDT; потеря прав во время офлайна → сервер отвергает обновления, клиент показывает «ваши изменения не приняты» и предлагает сохранить копию. - Enterprise on-prem: тот же
cloud-serviceвdocker compose; клиент выбирает сервер по домену организации.
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 Тайловый кэш
- Тайлы 256×256 px устройства, ключ
(page, tile_x, tile_y, scale_q, theme);scale_q— масштаб, квантованный до 1/64, чтобы при плавном зуме не перерисовывать на каждом кадре (между квантами тайлы масштабируются билинейно, резкость восстанавливается после остановки жеста). - Хранение: GPU-текстуры в атласе (бэкенд GPU) или
SkBitmap(CPU); LRU, лимит = min(25 % VRAM, 512 МБ) или 256 МБ CPU; предвыборка тайлов на 1 экран вперёд по направлению прокрутки. - Тайл рисуется из
DisplayListчерез R-tree-запрос пересекающих команд; растеризация в фоновом потоке бэкенда с приоритетом «видимые → предвыборка».
6.4 Слои
| Слой | Содержимое | Обновление |
|---|---|---|
| Документ | тайлы страниц, фон холста, тени страниц | по инвалидации |
| Выделение | текстовое выделение, выделение ячеек, рамки фигур | каждый кадр, векторно, дёшево |
| Оверлеи | курсоры (свой и чужие), маркеры комментариев, линейки, направляющие, подсказки | каждый кадр |
| Интерактив | перетаскиваемые объекты, рамки изменения размера | во время жеста, поверх всего |
Слои композитируются в DocumentCanvasItem (QQuickItem):
документ — текстура из TileCache, остальные —
DisplayList малого размера, рисуемые тем же бэкендом в ту
же цель кадра (QRhi
QRhiTexture/QSGTexture).
6.5 Текст
- Субпиксельное позиционирование: позиции глифов с точностью 1/4 px по
горизонтали (
SkFont::setSubpixel), раскладка всегда в EMU; на экране при масштабе < 150 % и Windows — хинтингslight, LCD-антиалиасинг по настройке ОС (ClearType); macOS — без хинтинга, grayscale AA (как система); Linux — настройки fontconfig (hintstyle,rgba,antialias). - Печать и PDF — без хинтинга, геометрия глифов векторная.
- Цветные эмодзи: COLRv1 → Skia нативно; sbix/CBDT (Apple Color Emoji,
Noto Color Emoji bitmap) → растровые глифы; fallback-шрифт выбирается
meridian-fonts::Fallbackпо письменности и наличию глифа. - Кэш шейпинга: ключ
(font, size, features, text run hash, lang, dir)→ShapedRun(глифы в font units и advance в EMU), LRU 64 МБ, разделяется между документами одного процесса.
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 Восстановление после сбоя
- Процесс окна упал → Hub получает
SIGCHLD/WaitForSingleObject→ перезапускает окно с флагом--recover <doc-id>→ документ восстанавливается из журнала автосохранения (§8.3) и показывает панель «Документ восстановлен; последние N секунд правок могли быть потеряны». Минидамп (crashpad) пишется вcrashes/и отправляется только по согласию (§10.7). - Codec-host упал/убит по лимиту →
Error::Format { .. }с текстом «файл повреждён или не поддерживается», предложение «открыть в защищённом режиме» (чтение с ослабленными проверками в новой песочнице с большим лимитом) и кнопка отправки файла в баг-репорт (с предупреждением о конфиденциальности). - Hub упал → окна продолжают работать автономно (локальные функции не
зависят от Hub); первое окно, которому понадобится Hub, перезапускает
его (единственность — файловая блокировка
hub.lock+ именованный mutex).
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| …):
- Временный файл в том же каталоге
.<name>.mtmp-<rand>, запись,fsync(file). - Перенос метаданных: права (
chmod), владелец при возможности, xattr (macOScom.apple.metadata:*, теги Finder; Linuxuser.*), ACL Windows (GetNamedSecurityInfo/SetNamedSecurityInfo), время создания. - Атомарная замена: POSIX
rename(2); WindowsReplaceFileW(сохраняет ACL и ADS, в том числеZone.Identifier?) — нет:ReplaceFileWпереносит потоки, кроме случаев, когда они явно удаляются; MOTW снимается только пользователем через «Разрешить редактирование» (§10.2); затемfsync(dir)на POSIX. - Если каталог недоступен для создания файлов (Samba с особыми
правами, папки с квотой), резервный путь: запись во временный файл в
$TMP, затем копирование поверх оригинала черезwrite-throughс резервной копией<name>.bakдо успешного завершения; пользователю показывается, что сохранение не атомарно. - Для облачных провайдеров (§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 Блокировки файлов
- Для MOP и любых документов, открытых на запись, создаётся соседний
файл
.~mlock.<name>(CBOR:{ user, host, pid, session_id, opened_at, app_version }), скрытый (dot-префикс / атрибут Hidden). При открытии: чужая живая блокировка (host совпадает и pid жив, или host другой и возраст < 24 ч) → диалог «Открыть только для чтения / Открыть копию / Уведомить, когда освободится»; устаревшая — снимается. - Для
docx/xlsx/pptxдополнительно создаётся совместимый с Office владелец-файл~$<name>.docx(структура MS: имя пользователя в ANSI и UTF-16), чтобы Word показывал, кем занят файл; чужой~$-файл уважается. - Дополнительно — рекомендательная блокировка диапазона 0..0
(
flock/LockFileExshared) на самом файле во время чтения и exclusive во время записи: на локальных FS даёт защиту от одновременногоrename. - Сетевые диски: SMB/NFS-блокировки ненадёжны, поэтому истина —
lock-файл + проверка перед сохранением: если
mtime/размер/BLAKE3 первых 64 КБ изменились с момента открытия → «Файл изменён на диске»: сравнить (если естьhistory/— трёхстороннее слияние через CRDT), перезаписать, сохранить как копию.
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 |
| 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
- QuickJS-ng через
rquickjs; отдельныйRuntimeна расширение,Contextна документ; памятьJS_SetMemoryLimit(64 МБ)(расширение может запросить до 256 МБ), стек 1 МБ,JS_SetInterruptHandler— прерывание через 5 с непрерывного выполнения без возврата в цикл событий (цикл в скрипте) с диалогом «Скрипт не отвечает: остановить?». - Нет
evalстрок из сети (fetchвозвращает данные,Function/evalотключены флагом в продакшн-режиме, включаются только в консоли разработчика), нет доступа кstd/osмодулям QuickJS, импорт — только из пакета расширения (ESM, статический граф проверяется при установке). - TypeScript: встроенный транспилятор (
swcв WASM? нет — тянет 10 МБ) — решение:oxc-транспилятор (Rust) как зависимостьmeridian-script, только стирание типов + современный синтаксис → ES2022; проверка типов — только в редакторе (LSP на основеtypescriptне встраиваем; редактор использует сгенерированные.d.tsдля автодополнения через собственный лёгкий анализатор +oxcпарсер). - Отладчик: QuickJS-ng debug API (точки останова, шаг, локальные переменные) → протокол, совместимый с DAP (Debug Adapter Protocol) по IPC; встроенный редактор показывает стек и переменные; внешние редакторы (VS Code) подключаются к DAP-порту при включённом «режиме разработчика».
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
cargo-fuzzцели на каждый кодек и парсер (fuzz_ooxml_docx,fuzz_rtf,fuzz_cfb,fuzz_pdf_lexer,fuzz_formula,fuzz_zip,fuzz_mop,fuzz_ipc_frame,fuzz_mxt_manifest), затравки —tests/corpus/fuzz-seeds, словари токенов; ночная сборка — 1 ч на цель с ASan+UBSan, непрерывный фаззинг — ClusterFuzzLite на выделенном раннере; падения → автоматический issue с минимизированным кейсом, кейс добавляется в регрессии.- Санитайзеры: ASan/UBSan для крейтов с
unsafe(raster, render, ffi) и для C++-оболочки; Miri — дляmeridian-core/meridian-textв ночной сборке;cargo carefulна тестах. - Статический анализ:
clippy -D warnings(+clippy::pedanticвыборочно),cargo-deny,cargo-audit,clang-tidyдля оболочки,semgrep-правила на опасные паттерны (unwrapв кодеках,std::processв ядре).
10.8 SBOM и политика зависимостей
- SBOM в формате CycloneDX генерируется на каждый релиз
(
cargo cyclonedx+ CMake-инвентаризация Qt/Skia), публикуется вместе с пакетами и подписывается; для Enterprise — поставляется в договоре. - Лицензии зависимостей ядра: разрешены MIT, Apache-2.0, BSD-2/3, ISC,
Zlib, MPL-2.0 (без модификаций), Unicode, OFL (шрифты); запрещены
GPL/AGPL в клиенте (LGPL — только Qt, динамически, по коммерческой или
LGPLv3 лицензии — см. вопросы к библии); проверка
cargo-denyв CI. - Обновления: Dependabot еженедельно, security-advisory — немедленно;
крупные зависимости (Skia, Qt, QuickJS, Yrs) — закреплены на конкретных
версиях с ручным апгрейдом раз в квартал и прогоном корпуса;
Cargo.lockиvcpkg/submodule-пины коммитятся;cargo vendorдля воспроизводимых офлайн-сборок Enterprise.
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 Как измеряем
tracingв ядре и оболочке с единымspan-деревом; экспорт в Perfetto (tracing-perfetto) по флагуMERIDIAN_TRACE=1, просмотр вui.perfetto.dev; в релизе — ограниченный набор span'ов для телеметрии (по согласию).criterionмикробенчмарки (kernel/benches): piece table, резолвер стилей, шейпинг, пересчёт, ZIP; история результатов вbench-historyветке, регрессия > 10 % блокирует merge (critcmpв CI).- Макробенчмарки —
tools/convert --benchпо наборуtests/corpus/perf/(50 файлов, представительные размеры), результаты в CSV → график на docs-сайте. - UI-бенчи — Squish-сценарии с замером кадров через
QT_LOGGING_RULES=qt.scenegraph.time.*.
11.3 Стратегии
- Ленивая загрузка: кодек выдаёт модель по частям
(
ModelChunkпо секциям/листам/слайдам); картинки декодируются при первом попадании в видимую область, мип-уровни по масштабу; шрифты — по требованию. - Фоновая раскладка: первая видимая страница — приоритет; остальной документ раскладывается в фоновом потоке по снимку (§4.5), нумерация страниц уточняется по мере готовности (как в Word); редактирование не ждёт фоновой раскладки.
- Кэши: шейпинг (§6.5), резолвер стилей (§4.2),
BlockDL(§6.2), тайлы (§6.3), результаты формул (значения хранятся, пересчёт — только грязных по графу зависимостей), миниатюры страниц. - Арены и интернирование: узлы в аренах с
генерационными индексами; строки атрибутов и имена стилей интернированы
(
Interner,Symbol(u32)); текст вPieceTableне перемещается. - SIMD в растре:
std::simd(при стабилизации) /wideдля композитинга, фильтров, конверсии цветов; тайлы 256² по потокам (rayon); AVX2/NEON черезmultiversion. - Параллелизм: раскладка независимых
секций/слайдов/листов — параллельно; пересчёт формул — по уровням
топологической сортировки; кодеки — разбор XML частей
(
word/document.xmlи стили/нумерация) параллельно. - Батчинг через FFI: модель → оболочка передаёт
пакеты изменений (
ChangeSet) раз в кадр, а не событие на каждыйOp; QML-модели (QAbstractItemModel) обновляются диапазонами.
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 Общее для всех платформ
- Локализация: Qt Linguist (
.ts→.qm) для оболочки,fluent-подобные ресурсы ядра для сообщений (locales/<lang>.ftl), ICU4X для чисел/дат/сортировки/плюрализации; первые языки — ru, en; целевые к 1.0 — de, fr, es, pt-BR, zh-CN, tr, kk, uz; RTL-интерфейс (ar, he) — фаза 2. - Шрифты в комплекте (библия §4): Liberation,
Carlito, Caladea, Noto (Sans/Serif/Mono + письменности + Color Emoji),
PT Sans/Serif, Inter, JetBrains Mono, STIX Two Math — ~120 МБ, грузятся
лениво; таблица подмены
fonts/substitutions.toml(Calibri → Carlito, Cambria → Caladea, Arial → Liberation Sans, Times New Roman → Liberation Serif, Courier New → Liberation Mono, Segoe UI → Inter/Noto Sans, …) с метрической совместимостью. - Обновления (
meridian-update): проверка раз в сутки через Hub, дельты zstd+bsdiff (обычно 5–15 МБ вместо 200), применение при следующем запуске; Windows MSIX — черезAppInstaller, MSI — наш апдейтер с UAC-службой (опц.); macOS — замена.appс проверкой подписи; Linux — Flatpak/репозитории сами, AppImage — наш апдейтер (zsync-подобные дельты). Каналы stable/beta/nightly, откат на предыдущую версию из Hub. - Аварийные отчёты:
crashpad(Rust-обвязка) → minidump →telemetry-service(Sentry-совместимый/api/<id>/minidump/), символизация на сервере (symbolic), символы для каждой сборки загружаются из CI; в on-prem — свой сервер или отключено. - Доступность: Qt Accessibility (UIA / NSAccessibility / AT-SPI2); холст документа отдаёт дерево доступности из модели (абзацы, таблицы, фигуры с alt-текстом), а не из пикселей.
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 Версионирование и ветки
- Продукт — semver
MAJOR.MINOR.PATCH;MAJOR— платная граница для вечных лицензий (библия §9.3);MINOR— функции (раз в 2–3 месяца);PATCH— исправления (по необходимости, в stable раз в 2 недели). - Ветки:
main— nightly-канал;release/1.x— отводится за 3 недели до1.x.0, получает только исправления (cherry-pick изmain), из неё — beta (1.x.0-beta.N) и stable; тегиv1.2.3. - Внутренние версии: FFI
MERIDIAN_ABI_VERSION, протокол IPC, формат MOP (manifest/@version), MeridianScript API (apiVersion) — свои счётчики с правилами совместимости вspec/. - Воспроизводимые сборки:
SOURCE_DATE_EPOCH,--remap-path-prefix,-C codegen-units=1,Cargo.lock, закреплённый toolchain, детерминированный ZIP/MSI/DMG (пакетные инструменты с фиксированными метками времени), сборка в контейнере; сравнение хэшей двух независимых сборок вrelease-job.
14. Интеграция лицензирования в архитектуру
Детали ключей, активации, сервера и портала —
10-licensing-accounts.md; здесь — только то, как лицензия
живёт в коде.
- Единая точка:
meridian_license::Features(битовое множествоu128с именами флагов из10-licensing-accounts.md§2,has(Feature); флаги ядраis_core()не гейтятся никогда), вычисляется при старте изlicense.mlic(License::evaluate(now, last_check, fingerprint)) и редакции; без лицензии —Features::for_edition(Edition::Free); пробный период —Edition::Trialсexpires_at. - Точки проверки (все — через
Features, никаких «своих» флагов):- запуск приложения:
apps/<app>/app.tomlобъявляетrequires = ["notes"]; Hub не показывает/запускает приложение без функции, окно показывает «замок» с предложением редакции; - реестр команд:
Command { feature: Option<Feature> }— команда отображается с замком, выполнение возвращаетCommandError::FeatureLocked→ диалог апгрейда; - кодеки:
Exporter::required_feature()(напримерPdfEditдля записи PDF с правками); - скрипты: API-методы с
#[feature(Scripts)], магазин —ScriptStore; - облако/совместная работа:
meridian-syncне подключается безCloud/Collaboration; - FFI:
meridian_features() -> u64для оболочки; биты не записываются обратно.
- запуск приложения:
- Многократная проверка: лицензия валидируется в ядре
(подпись, сроки), повторно — при обращении к
Featuresв оболочке (контрольная сумма структуры в памяти раз в N минут), и вmeridian-sync/meridian-idпри сетевых операциях (сервер проверяет сам). Цель — «честным удобно, пиратам неудобно» (библия §9.3). - Офлайн: вечная лицензия — проверка только подписи и
мажорной версии; подписка —
check_interval_days = 7, затемgrace_days = 30(LicenseStatus::OfflineGrace), затемLicenseStatus::Expired→DocumentMode::ReadOnlyPrint(документы открываются, печатаются, экспортируются в PDF; редактирование заблокировано с баннером). Плавающая — аренда 1 ч с фоновым продлением через Hub и 24 ч офлайн-резервом (FloatingLease). - Хранение:
license.mlicв<data>/license/, отпечаток устройства (algorithms/kernel-core.md§11) пересчитывается при старте; смена ≥ 2 компонентов отпечатка → требование повторной активации (онлайн или через портал). - UI: один компонент
FeatureGateв QML (замок + подсказка + кнопка «Сравнить редакции»), один диалог апгрейда; состояние лицензии — в строке состояния Hub и в backstage «Сведения».
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. План:
- Держать мост тонким с первого дня: QObject-модели только для того,
что видит QML (списки, свойства, команды); всё остальное — через C ABI
meridian-ffiсcbindgen-заголовком и батчингом (ChangeSetраз в кадр). - Критерий отказа: > 20 % времени кадра в мосте на бенче «ввод текста» или блокирующие баги без фикса > 3 мес.
- Запасной путь: ручные QObject-обёртки на C++ поверх C ABI (как
сейчас у
meridian_document_*) — больше кода, но полный контроль; генерация обёрток собственным скриптом из тех же деклараций, что.d.ts(tools/ts-typesрасширяется до C++). - Крайний случай: холст и большая часть панелей рендерятся самим ядром
(Skia) внутри одного
QQuickItem, QML остаётся для ribbon/диалогов — «Krita-подход».
15.3 Стратегия отказа от Skia в пользу vello
Skia — большой C++-проект (сборка 20+ мин, ~30 МБ бинарей, зависимости от Clang/Ninja/GN); vello (wgpu) — Rust, компактный, GPU-compute растеризация, но без зрелого текста, PDF-бэкенда и CPU-резерва.
- Абстракция с первого дня:
trait Backend { fn render(&mut self, dl: &DisplayList, target: Target) };DisplayListне содержит Skia-типов (пути — свойPath, глифы — id + позиции). - Этапы: (1) фаза 1 — Skia для всего; (2) фаза 3 —
velloэкспериментальный бэкенд для экрана за флагом (MERIDIAN_RENDER=vello), сравнение снимков корпуса с порогом; (3) при паритете по тексту (субпиксельное позиционирование, хинтинг, цветные шрифты COLRv1/bitmap) и производительности на интегрированных GPU — vello по умолчанию для экрана, Skia остаётся для PDF/печати/снимков (CPU-детерминизм) до появления эквивалента (vello_cpu+ свой PDF-писатель вcodec-pdf). - Критерии: ≤ 0,5 % пикселей расхождения с Skia на корпусе, 60 к/с на Intel UHD 620 при 4K, память ≤ Skia.
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 |
Вопросы к библии
- Лицензия исходного кода. Библия описывает
коммерческий продукт, но не говорит, открыт ли код (open core /
проприетарный / source-available). От этого зависят
licenseвCargo.toml, политика зависимостей (§10.8) и выбор LGPLv3 против коммерческой лицензии Qt. Предложение: ядро и кодеки — открыты (Apache-2.0), оболочка, Assist, сервисы — проприетарные; решение нужно до фазы 1. - Qt: LGPLv3 или коммерческая. LGPLv3 требует динамической линковки и возможности замены Qt пользователем, что усложняет App Store-сборку и MSIX; коммерческая лицензия — ежегодная плата за разработчика. Нужно решение владельца с оценкой бюджета.
- App Store для macOS и Microsoft Store. App Sandbox
ограничивает процессную модель (§7): помощники должны быть XPC-сервисами
внутри бандла, без
fork/execпроизвольных процессов; Store-версия, вероятно, будет без Python-плагина и с ограниченными портами. Подтвердить, нужны ли Store-сборки в 1.0 или только прямые. - Минимальные версии ОС. Библия: Windows 10/11, macOS 13+, Linux без уточнения. Предложение зафиксировать: Windows 10 1809+ (D3D12 feature level 11, MSIX), glibc 2.31+ (Ubuntu 20.04+), Wayland и X11.
- Единицы в MOP. Библия говорит о «XML по духу
OOXML/ODF». Предложение: в MOP-XML писать длины в EMU целыми (как
DrawingML) с необязательным суффиксом для читаемости в плоском варианте
(
"12700emu"), а не в десятичныхcm— это сохраняет детерминизм §3.1 и обратимость. Утвердить вspec/mop. - Paint и совместная работа. Библия относит CRDT к «Meridian Sync» без перечисления приложений; архитектура исключает пиксели Paint из CRDT (§5.1). Подтвердить, что совместная работа нужна для Write/Sheets/Slides/Draw/ Notes/Forms, а Paint/PDF/Mail/Base — вне её.
- Веб-панели расширений. Библия обещает «расширяемость — часть продукта», архитектура запрещает WebView в расширениях (§9.3) ради безопасности и размера. Если магазину нужны богатые панели, потребуется решение о встроенном WebEngine (+150 МБ) — предлагается отложить до фазы 7.
Meridian Printкак отдельный модуль. Библия перечисляет Print среди модулей ядра; здесь это тонкий крейтmeridian-printнадmeridian-render+ ОС. Если ожидается больший объём (управление очередями, профили принтеров, цветокоррекция), выделить задачу в08-roadmap.md.- Телеметрия по умолчанию в Trial. Для оценки конверсии триала полезны метрики, но библия требует согласия. Оставляем «только по согласию» и спрашиваем в первом запуске; подтвердить.
- Нумерация документов. Файл
03-architecture.mdссылается на10-licensing-accounts.mdи08-roadmap.md(ещё не написаны) и наdocs/algorithms/; имена закрепить в08-roadmap.md/оглавлении docs.