Meridian Forms — формы и опросы
Документ приложения по соглашениям раздела 8 библии (
docs/00-foundation.md). Версия 0.1 · 2026-10-03. Фаза 5 · редакции Home и выше (Feature::Forms— личные формы; сбор ответов через Meridian Cloud —Feature::Cloud, Pro+, фаза 6; корпоративные формы с SSO-ответами — Business+) · форматы.mfx/.mft·urn:meridian:office:forms:1· цвет--app-forms#5C940D· значок «список с галочкой» · идентификаторы экрановFM-xx· макетmockups/forms.html.
Содержание: 1. Назначение и аналоги · 2. Функции · 3. Экраны · 4. Модель данных · 5. Алгоритмы и скрипты · 6. Форматы · 7. Горячие клавиши · 8. Критерии готовности · 9. Риски · 10. Вопросы к библии.
1. Назначение и аналоги
Meridian Forms — конструктор форм, опросов, анкет, регистраций и тестов. Особенность: это десктопное приложение — конструктор, логика, предпросмотр, результаты и аналитика работают локально и офлайн, а сбор ответов идёт одним из каналов:
| Канал сбора | Как работает | Фаза / редакция |
|---|---|---|
| Meridian Cloud | форма публикуется на cloud.meridian-office.<tld>
(или on-prem сервер организации); респонденты отвечают в браузере;
ответы приходят в приложение по синхронизации |
фаза 6, Pro+ |
| PDF-форма с полями | экспорт в PDF с AcroForm (текст, флажки, переключатели, списки, подпись); респондент заполняет в любом PDF-просмотрщике и присылает файл; ответы импортируются | фаза 5, Home+ |
| Письмо с ответом в Mail | рассылка через Meridian Mail: письмо с формой (HTML с полями,
mailto:-ответ) или с вложенным PDF; ответы
(письма/вложения) автоматически разбираются Mail и передаются в
Forms |
фаза 5, Home+ |
| Импорт ответов из файлов | .xlsx/.csv/.msx (экспорт других сервисов — Google
Forms, MS Forms, Яндекс Формы), заполненные PDF, .mfr
(пакет ответов Meridian) |
фаза 5, Home+ |
| Киоск | режим «одно устройство — много респондентов» (планшет/ПК на стойке): форма открывается во весь экран, ответы сохраняются локально | фаза 5, Home+ |
| Аналог | Сильные стороны | Слабости, которые закрываем |
|---|---|---|
| Microsoft Forms | интеграция с M365, тесты | только онлайн, слабое ветвление, нет локальных данных, нет печати |
| Google Forms | простота, Sheets | только онлайн, нет AcroForm, ограниченная логика, нет тем кроме цвета |
| Typeform | дизайн, «по одному вопросу» | дорого, онлайн |
| Яндекс Формы | РФ-хостинг | онлайн, ограниченная аналитика |
| LimeSurvey | мощные опросы, self-hosted | сложный, веб-only, устаревший UI |
Отличия Meridian Forms: конструктор и данные локально; несколько
каналов сбора, включая без сервера; единая модель формы для веб-ответа,
PDF и печати; живая связь результатов с Sheets .msx;
диаграммы Meridian Chart; тесты с баллами; многоязычные формы; ветвление
с графом и проверкой на ошибки; MeridianScript.
Связи в пакете: Sheets (результаты как таблица, обновление по мере поступления ответов), Mail (рассылка, приём ответов), Chart (сводка), PDF (экспорт AcroForm, разбор заполненных форм), Write (печать формы как документа — P1), Base (ответы в таблицу — P1), Hub (шаблоны форм, уведомления «N новых ответов»).
Код: kernel/crates/meridian-forms/ (модель, логика,
валидация, подсчёт, агрегация, рендереры), codecs/forms/
(.mfx, импорт вопросов из .docx/.xlsx, экспорт
AcroForm), shell/src/forms/ (QML/C++),
services/cloud-service/forms/ (веб-рендерер и приём
ответов, фаза 6).
2. Функции
2.1 Типы вопросов
Тип (type) |
Описание | Параметры | Приоритет |
|---|---|---|---|
short-text |
однострочный текст | плейсхолдер, валидация (длина, regex, число/e-mail/телефон/URL —
подтипы email, phone, url,
number) |
P0 |
long-text |
многострочный | мин/макс длина, счётчик символов | P0 |
single-choice |
один из вариантов | варианты, «другое» с полем, перемешивание, вертикально/в строку, изображения у вариантов | P0 |
multi-choice |
несколько | мин/макс выбранных, «другое», «ничего из перечисленного» (эксклюзивный вариант) | P0 |
dropdown |
раскрывающийся список | варианты (в т.ч. из файла/списка: страны, регионы), поиск | P0 |
scale |
линейная шкала | от/до (0–10), подписи концов, шаг | P0 |
rating |
рейтинг | звёзды/сердца/цифры, 3–10 | P0 |
date, time, datetime |
дата/время | мин/макс, формат по локали, «включая время» | P0 |
number |
число | мин/макс, шаг, целое/десятичное, единицы, слайдер | P0 |
file |
загрузка файла | типы, размер, количество (только Cloud и импорт; в PDF — недоступно, в письме — вложение) | P1 |
likert |
таблица утверждений × шкала согласия | строки, столбцы (шкала), «н/д» | P0 |
ranking |
ранжирование перетаскиванием | варианты, «ранжировать первые N» | P1 |
matrix |
матрица: строки × столбцы | один/несколько в строке, текстовые ячейки (P2) | P0 |
nps |
0–10 с группами детракторы/нейтралы/промоутеры | подписи | P0 |
yes-no |
да/нет | подписи | P0 |
signature |
подпись | рисование (перо/мышь/палец), печатная фамилия, дата | P1 |
section |
заголовок раздела с описанием | заголовок, текст, изображение | P0 |
page-break |
разделитель страниц | заголовок страницы | P0 |
description |
блок текста/изображения/видео-ссылки (без ответа) | форматированный текст (Write), медиа | P0 |
consent |
согласие (обязательный флажок с текстом) | текст, ссылка на политику | P0 |
hidden |
скрытое поле (из URL-параметра/источника рассылки) | имя параметра | P1 |
2.2 Логика, валидация, настройки
| Функция | Описание | Приоритет |
|---|---|---|
| Ветвление | правила перехода после страницы/вопроса: «если ответ = X → перейти на страницу P / к вопросу Q / завершить (с текстом)»; условия: равно/не равно/содержит/больше/меньше/пусто/выбрано/любое из; составные И/ИЛИ | P0 |
| Условные вопросы | показывать вопрос/раздел, если условие по предыдущим ответам (на той же или предыдущих страницах) | P0 |
| Вычисляемые поля | выражения над ответами (={q3} * {q4}, функции движка
выражений Sheets: SUM, IF, TEXT,
даты) для показа, подсчёта и ветвления |
P1 |
| Подстановка ответов | {q1} в тексте последующих вопросов («Спасибо,
{q1}!») |
P0 |
| Валидация | обязательность, типовая (e-mail, телефон E.164, URL, число, дата), regex с сообщением, диапазоны, длина, мин/макс выбранных, уникальность ответа (Cloud) | P0 |
| Перемешивание | вариантов (все/кроме последних N), вопросов на странице, страниц | P0 |
| Лимиты | число ответов, срок приёма, один ответ на респондента (Cloud: по аккаунту/cookie), квоты по варианту («не более 50 выбравших A» — P2) | P0 (квоты — P2) |
| Темы | цвет, шрифт (из комплекта), фон/обложка, логотип, скругления, тёмная тема; корпоративные темы из общей папки; предпросмотр телефон/планшет/ПК | P0 |
| Многоязычность | переводы текстов формы (вопросы, варианты, сообщения) по языкам; выбор языка респондентом/по браузеру; экспорт/импорт переводов XLIFF/XLSX | P1 |
| Тесты | режим «тест»: баллы за вопрос, правильные ответы (single/multi/dropdown/short-text по шаблону/number), частичные баллы, пояснения, показ результата (сразу/после проверки), проходной балл, лимит времени (Cloud), случайный набор из банка вопросов (P2) | P0 (банк — P2) |
| Доступ | кто может отвечать: все по ссылке / с Meridian ID / сотрудники организации (SSO) / по списку e-mail (приглашения с индивидуальными ссылками); анонимность (не собирать e-mail/имя) или именные ответы; «разрешить редактировать ответ»; «показать сводку респонденту» | P0 (организация — Business) |
| Сообщения | стартовая страница, страница «спасибо» с кнопкой/ссылкой/перенаправлением, сообщение «форма закрыта» | P0 |
| Прогресс | индикатор, нумерация вопросов, «по одному вопросу на экран» (режим Typeform) | P0 (по одному — P1) |
| Автосохранение ответа | у респондента (localStorage в вебе / файл в киоске) с продолжением | P1 |
| Уведомления | «новый ответ» — в Hub/почтой/дайджест | P1 |
2.3 Сбор ответов и результаты
| Функция | Описание | Приоритет |
|---|---|---|
| Ссылка и QR | короткая ссылка Cloud, QR-код (PNG/SVG, печать на плакат), параметры
предзаполнения ?q1=… |
P0 (Cloud — фаза 6) |
| Встраивание | <iframe>/JS-сниппет для сайта |
P1 |
| Рассылка через Mail | выбор получателей (контакты/группы/Sheets), шаблон письма, индивидуальные ссылки или вложенный PDF/HTML-форма, отслеживание «ответил/не ответил», напоминания | P0 (ссылки — фаза 6) |
| Киоск | полноэкранный режим с автосбросом через N секунд бездействия, ПИН для выхода | P1 |
| Приём ответов | синхронизация с Cloud; разбор входящих писем (Mail передаёт
.mfr/PDF/HTML-ответ); импорт файлов; ручной ввод оператором
(бумажные анкеты) |
P0 |
| Сводка | по каждому вопросу: диаграмма (столбцы/круг/гистограмма/NPS-шкала/тепловая карта матрицы/облако слов — P2), средние, медиана, распределение; фильтры по ответам, датам, источникам; сравнение сегментов | P0 |
| По респондентам | таблица ответов (столбец — вопрос), карточка респондента, поиск, пометки, удаление, экспорт одного ответа в PDF | P0 |
| Тесты | баллы, распределение, самые сложные вопросы, по респондентам, экспорт ведомости | P0 |
| Живая связь с Sheets | «Открыть в Sheets»: .msx с таблицей ответов,
обновляемой при новых ответах (внешний источник данных Sheets
forms://<form-id>), формулы и сводные поверх |
P0 |
| Экспорт | XLSX/CSV/.msx (ответы), PDF (сводка с диаграммами;
отдельный ответ), JSON, .mfr |
P0 |
| Печать формы | бумажная версия: все вопросы, поля для рукописного ответа, квадраты для отметки, номера; QR на бланке для сопоставления; «версия для сканирования» с якорями (распознавание — P2) | P0 (распознавание — P2) |
| Печать с ответами | заполненная форма респондента | P0 |
2.4 Прочее
Шаблоны форм (регистрация, обратная связь, опрос сотрудников, NPS,
тест, заявка, бронирование, викторина) из галереи Hub; импорт вопросов
из .docx/.xlsx (нумерованный список → вопросы, таблица →
варианты); копия формы; версии формы (при изменении опубликованной —
новая версия, ответы хранят версию); совместное редактирование формы
(фаза 6); Assist (P2: «сгенерировать опрос по теме», «подытожить
открытые ответы»); доступность формы для респондентов (WCAG 2.1 AA в
веб-рендере: подписи, фокус, контраст, экранный диктор).
3. Экраны
Каркас окна — по разделу 5.3 библии. Ribbon Forms: Файл · Главная · Вставка · Логика · Оформление · Ответы · Скрипты. Вверху окна — переключатель режимов Вопросы · Логика · Оформление · Предпросмотр · Ответы (как вкладки документа).
| ID | Экран | Описание |
|---|---|---|
| FM-01 | Конструктор (Вопросы) | Слева: структура (страницы → вопросы, перетаскивание, свёртка); центр: лента вопросов как в форме (редактирование на месте: текст, варианты, параметры), между вопросами «+» с галереей типов; справа: панель свойств выбранного вопроса (тип, обязательность, валидация, перемешивание, баллы, подсказка, медиа, логика «показывать если»); нижняя строка: «N вопросов · M страниц · ~K мин» |
| FM-02 | Панель вопроса | Расширенные свойства по типу: варианты (массовый ввод — по строке, импорт списка), «другое», изображения вариантов, шкала (мин/макс/подписи), строки/столбцы матрицы, маска/regex, правильные ответы и баллы (в тесте), пояснение, переводы (вкладка) |
| FM-03 | Логика и ветвление | Визуальный граф: узлы — страницы/вопросы, рёбра — переходы с условиями; список правил справа («после страницы 2: если {q3} = «Нет» → страница 5»); панель проверок (циклы, недостижимые страницы, вопросы, на которые ссылаются условия после их пропуска) с переходом к проблеме; «Симулятор пути» (выбрать ответы → подсветить маршрут) |
| FM-04 | Оформление | Галерея тем, цвет/шрифт/обложка/логотип, параметры (скругления, прогресс, нумерация), тёмная тема, предпросмотр ПК/планшет/телефон; «Сохранить как тему организации» |
| FM-05 | Предпросмотр | Форма как увидит респондент (веб-рендер в встроенном движке или PDF-вид), переключатель устройств, язык, «пройти как респондент» (ответы не сохраняются / сохранить как тест) |
| FM-06 | Настройки формы | Вкладки: Общие (название, описание, сообщения, прогресс, по одному вопросу), Ответы (кто может, один ответ, анонимность, редактирование, лимиты, срок), Тест (режим, проходной балл, показ результата, время), Языки, Уведомления, Версии (список, «сделать текущей», различия) |
| FM-07 | Публикация и сбор | Каналы: Meridian Cloud (опубликовать/обновить/закрыть, ссылка, QR, встраивание, статус сервера), PDF-форма (экспорт), Рассылка (запуск мастера FM-14), Киоск (запуск); статистика по каналам |
| FM-08 | Ответы — сводка | Карточки по вопросам с диаграммами Chart и числами; фильтр (период, источник, сегмент по ответу, язык, версия формы); «Открыть в Sheets», «Экспорт PDF», переключение типа диаграммы на карточке |
| FM-09 | Ответы — по респондентам | Таблица (виртуализированная): дата, источник, респондент, баллы, статус (полный/частичный), ответы; карточка ответа (все вопросы с ответами, вложения, путь по форме), печать/PDF/удалить/пометить; ручной ввод «+ Ответ» |
| FM-10 | Тест — результаты | Ведомость (респондент, баллы, %, зачёт), распределение баллов, по вопросам (% правильных, самые сложные), проверка открытых вопросов вручную (режим «проверка»: выставить баллы, комментарий), экспорт |
| FM-11 | Переводы | Таблица: исходный текст · перевод по языкам (ячейки редактируются), статус «не переведено», импорт/экспорт XLIFF/XLSX, «перевести через Assist» (P2) |
| FM-12 | Импорт ответов | Источник
(XLSX/CSV/.msx/PDF-папка/.mfr/письма из Mail)
→ сопоставление столбцов с вопросами (авто по заголовкам/ID) →
предпросмотр → дедупликация (по response_id/хэшу) →
отчёт |
| FM-13 | Экспорт PDF-формы | Параметры: размер страницы, поля AcroForm/бумажная версия, QR для сопоставления, язык, «защитить от изменения структуры»; предпросмотр |
| FM-14 | Мастер рассылки | Получатели (контакты/группы/Sheets/CSV), способ (ссылка Cloud · PDF
во вложении · HTML-форма в письме), шаблон письма с полями
{{имя}} и {{ссылка}}, предпросмотр, отправка в
очередь Mail, отслеживание |
| FM-15 | Печать | Предпросмотр Meridian Print: пустая форма / с ответами / сводка; параметры (колонки, QR, поля для записи) |
| FM-16 | Параметры Forms | Общие (язык по умолчанию, автор), Оформление (тема по умолчанию), Cloud (сервер, аккаунт), Mail (идентификация для рассылок, папка приёма), Киоск (ПИН, таймаут), Конфиденциальность (хранить IP/агент — выкл. по умолчанию) |
| FM-17 | Киоск | Полноэкранная форма, крупные элементы, «Начать заново», автосброс, выход по ПИН |
| FM-18 | Галерея типов вопросов | Всплывающая галерея при «+»: типы с иконками и описанием, поиск, недавние |
4. Модель данных
4.1 Пакет .mfx
survey.mfx (ZIP, mimetype = application/vnd.meridian.forms)
├── mimetype
├── manifest.xml ← части, версия формы (form-version), хэши
├── meta.xml ← Dublin Core + forms:* (владелец, организация, публикация)
├── form.xml ← форма: настройки → страницы → вопросы → варианты; логика; оценивание
├── theme.xml ← тема оформления (токены MDS + переопределения)
├── translations/<lang>.xml ← переводы по ключам (question/option/message id)
├── media/<sha256>.<ext> ← изображения, логотип, вложения к вопросам
├── responses/
│ ├── <source-ulid>.xml ← пакеты ответов по источникам (append-only): cloud, mail, import, manual, kiosk
│ └── grading.xml ← ручные оценки открытых вопросов
├── history/form-v<N>.xml ← предыдущие версии формы (структура, на которую ссылаются ответы)
├── scripts/ ← MeridianScript формы (только для десктопа; на сервере не исполняются)
├── signatures/
└── thumbnail.png
Шаблон .mft — тот же пакет без responses/ и
history/, с manifest.xml/@template="true".
Пакет ответов .mfr — ZIP с
responses/<source>.xml + media/
(вложения респондентов) + manifest.xml со ссылкой на
form-id и form-version; им обмениваются канал
Mail, киоск и импорт.
4.2 form.xml
<form xmlns="urn:meridian:office:forms:1" id="01J9X0.." version="3" lang="ru" title="Опрос сотрудников 2026"
mode="survey" theme="theme.xml" progress="bar" numbering="true" one-question-per-page="false" shuffle-pages="false">
<settings>
<access who="organization" one-response="true" anonymous="false" allow-edit="true" show-summary="false"/>
<limits max-responses="500" closes="2026-11-01T00:00:00Z"/>
<messages start="Ответы займут 5 минут" thanks="Спасибо! Ваш ответ сохранён." closed="Приём ответов завершён"/>
<notify on-response="hub" digest="daily"/>
</settings>
<page id="p1" title="О вас">
<question id="q1" type="single-choice" required="true" shuffle="true">
<text>В каком отделе вы работаете?</text>
<option id="o1">Продажи</option><option id="o2">Разработка</option><option id="o3">Поддержка</option>
<option id="o9" other="true">Другое</option>
</question>
<question id="q2" type="nps" required="true"><text>Насколько вероятно, что вы порекомендуете компанию как работодателя?</text>
<scale min="0" max="10" min-label="Маловероятно" max-label="Очень вероятно"/></question>
</page>
<page id="p2" title="Поддержка">
<question id="q3" type="likert" show-if="{q1} = 'o3'">
<text>Оцените утверждения</text>
<rows><row id="r1">Инструменты удобны</row><row id="r2">Нагрузка посильна</row></rows>
<columns><col id="c1" value="1">Не согласен</col><col id="c2" value="2"/><col id="c3" value="3"/><col id="c4" value="4"/><col id="c5" value="5">Согласен</col></columns>
</question>
<question id="q4" type="long-text" required="false" max-length="2000"><text>Что улучшить? {q1}</text></question>
</page>
<page id="p3" title="Тест по безопасности">
<question id="q5" type="multi-choice" points="2" partial="true">
<text>Какие пароли надёжны?</text>
<option id="a1" correct="true">tr0ub4dor&3-horse-battery</option><option id="a2">123456</option><option id="a3" correct="true">фраза из пяти слов</option>
<explanation>Надёжны длинные фразы.</explanation>
</question>
<question id="q6" type="short-text" subtype="email" required="true" validation="regex" pattern="^[^@]+@company\.ru$" message="Укажите корпоративный адрес"/>
</page>
<logic>
<!-- Правила перехода: оцениваются по порядку после страницы / Jump rules evaluated in order after a page -->
<rule after="p1" if="{q1} = 'o3'" goto="p2"/>
<rule after="p1" if="{q2} <= 6" goto="p2"/>
<rule after="p1" goto="p3"/> <!-- без условия = по умолчанию / default -->
<rule after="p2" goto="p3"/>
<rule after="p3" goto="end"/>
<computed id="c_score_pct" expr="=ROUND({score} / {max_score} * 100)"/>
</logic>
<scoring enabled="true" pass="60" show-result="immediately" time-limit="PT20M"/>
</form>Условия (if, show-if) — выражения движка
выражений пакета над ссылками {qN} (значение ответа: id
варианта, список id, число, текст, дата), {qN.other},
{score}, {page}, {lang},
{param.name} (скрытые поля). Операторы: =,
<>, <, >,
<=, >=, CONTAINS,
IN, ISBLANK, AND,
OR, NOT.
4.3 Ответы
(responses/<source>.xml)
<responses xmlns="urn:meridian:office:forms:1" form="01J9X0.." source="cloud" exported="2026-10-03T10:00:00Z">
<response id="01J9X5.." form-version="3" started="2026-10-02T09:12:03Z" submitted="2026-10-02T09:17:40Z" lang="ru"
status="complete" respondent="user:01J8..|anon|email:ivan@company.ru" channel="cloud|mail|pdf|kiosk|import|manual"
score="7" max-score="10" passed="true" path="p1,p2,p3">
<answer q="q1"><choice>o3</choice></answer>
<answer q="q2"><number>8</number></answer>
<answer q="q3"><cell row="r1" col="c4"/><cell row="r2" col="c2"/></answer>
<answer q="q4"><text>Нужен второй монитор.</text></answer>
<answer q="q5"><choice>a1</choice><choice>a3</choice></answer>
<answer q="q6"><text>ivan@company.ru</text></answer>
<meta key="ua">…</meta> <!-- только при включённом сборе / only when enabled -->
</response>
</responses>4.4 Структуры ядра (Rust)
// kernel/crates/meridian-forms/src/model.rs
pub struct Form { pub id: Ulid, pub version: u32, pub title: LString, pub settings: Settings, pub pages: Vec<Page>,
pub logic: Logic, pub scoring: Option<Scoring>, pub theme: Theme, pub langs: Vec<Lang> }
pub struct Page { pub id: Id, pub title: LString, pub questions: Vec<Question>, pub show_if: Option<Expr> }
pub struct Question {
pub id: Id, pub kind: QuestionKind, pub text: LString, pub description: Option<LString>, pub media: Option<MediaRef>,
pub required: bool, pub show_if: Option<Expr>, pub validation: Validation, pub shuffle: bool,
pub points: Option<f64>, pub partial: bool, pub explanation: Option<LString>,
}
pub enum QuestionKind {
ShortText { subtype: TextSubtype, max_len: Option<u32> }, LongText { max_len: Option<u32> },
SingleChoice { options: Vec<Option_>, other: bool, layout: Layout }, MultiChoice { options: Vec<Option_>, other: bool, min: Option<u8>, max: Option<u8> },
Dropdown { options: Vec<Option_> }, Scale { min: i8, max: i8, step: u8, labels: (LString, LString) }, Rating { max: u8, icon: RatingIcon },
Date, Time, DateTime, Number { min: Option<f64>, max: Option<f64>, step: Option<f64>, integer: bool },
File { types: Vec<String>, max_size: u64, max_count: u8 }, Likert { rows: Vec<Row>, cols: Vec<Col>, na: bool },
Ranking { options: Vec<Option_>, top_n: Option<u8> }, Matrix { rows: Vec<Row>, cols: Vec<Col>, multi: bool },
Nps, YesNo, Signature, Section, PageBreak, Description, Consent { text: LString, link: Option<Url> }, Hidden { param: String },
}
pub struct Option_ { pub id: Id, pub text: LString, pub image: Option<MediaRef>, pub correct: bool, pub exclusive: bool }
pub struct Logic { pub rules: Vec<JumpRule>, pub computed: Vec<Computed> }
pub struct JumpRule { pub after: Id /* page */, pub cond: Option<Expr>, pub target: Target /* Page(Id) | Question(Id) | End(Option<LString>) */ }
pub enum AnswerValue { Choice(Vec<Id>), Other(String), Text(String), Number(f64), Date(NaiveDate), Time(NaiveTime), DateTime(DateTime<Utc>),
Cells(Vec<(Id, Id)>), Ranking(Vec<Id>), Files(Vec<MediaRef>), Signature(MediaRef), Bool(bool), Empty }
pub struct Response { pub id: Ulid, pub form_version: u32, pub started: Timestamp, pub submitted: Option<Timestamp>, pub status: Status,
pub respondent: Respondent, pub channel: Channel, pub lang: Lang, pub answers: BTreeMap<Id, AnswerValue>,
pub score: Option<Score>, pub path: Vec<Id>, pub meta: BTreeMap<String, String> }4.5 Кэш в профиле
(<profile>/forms/index.db)
Производные данные для быстрых сводок и фильтров (перестраиваются из пакета):
CREATE TABLE forms (id TEXT PRIMARY KEY, path TEXT, title TEXT, version INTEGER, published INTEGER, cloud_id TEXT, responses INTEGER, last_response INTEGER);
CREATE TABLE responses (id TEXT PRIMARY KEY, form_id TEXT, form_version INTEGER, submitted INTEGER, channel TEXT, respondent TEXT, status TEXT, score REAL, lang TEXT);
CREATE TABLE answers (response_id TEXT REFERENCES responses(id) ON DELETE CASCADE, q TEXT, kind TEXT, v_text TEXT, v_num REAL, v_id TEXT, row_id TEXT, col_id TEXT);
CREATE INDEX answers_q ON answers(q, v_id); CREATE INDEX answers_num ON answers(q, v_num);
CREATE TABLE sync_state (form_id TEXT PRIMARY KEY, cloud_cursor TEXT, mail_folder TEXT, last_sync INTEGER);
CREATE TABLE dedupe (form_id TEXT, fingerprint BLOB, response_id TEXT, PRIMARY KEY(form_id, fingerprint));5. Алгоритмы и скрипты
5.1 Граф ветвления: циклы и недостижимость
Узлы — страницы (p*), плюс start и
end; рёбра — правила перехода (условные и по умолчанию) и
«следующая страница» (если у страницы нет правила без условия). Переходы
«к вопросу» приводятся к странице вопроса с пометкой «прокрутка».
pub fn check_logic(form: &Form) -> Vec<LogicIssue> {
let g = build_graph(form); // adjacency: page → [(cond, target)]
let mut issues = vec![];
// 1. Циклы — DFS с тремя цветами; цикл допустим только если на нём есть условное ребро,
// но и тогда предупреждение «возможный бесконечный маршрут» / cycles: DFS, 3 colours
let mut colour = HashMap::new();
fn dfs(n: Id, g: &Graph, colour: &mut HashMap<Id, u8>, stack: &mut Vec<Id>, issues: &mut Vec<LogicIssue>) {
colour.insert(n, 1); stack.push(n);
for (cond, t) in &g[&n] {
match colour.get(t).copied().unwrap_or(0) {
0 => dfs(*t, g, colour, stack, issues),
1 => issues.push(if cond.is_none() { LogicIssue::Cycle(stack.clone(), *t) } else { LogicIssue::PossibleLoop(stack.clone(), *t) }),
_ => {}
}
}
stack.pop(); colour.insert(n, 2);
}
let mut stack = vec![]; dfs(START, &g, &mut colour, &mut stack, &mut issues);
// 2. Недостижимые страницы — всё, что не покрашено после DFS от start / unreachable = not visited
for p in &form.pages { if !colour.contains_key(&p.id) { issues.push(LogicIssue::Unreachable(p.id)); } }
// 3. Нет пути к end (тупики: страница без исходящих рёбер, кроме end) / dead ends
for (n, edges) in &g { if edges.is_empty() && *n != END { issues.push(LogicIssue::DeadEnd(*n)); } }
// 4. Условие ссылается на вопрос, который может быть не отвечен на всех путях к этой точке
// / condition references a question possibly unanswered on some path
for (n, edges) in &g { for (cond, _) in edges { if let Some(c) = cond { for q in c.question_refs() {
if !dominates(&g, page_of(form, q), *n) { issues.push(LogicIssue::MaybeUnanswered { at: *n, q }); } } } } }
// 5. Обязательный вопрос на недостижимой странице, правило «по умолчанию» не последнее (затеняет), пустые ветви "other"
issues.extend(static_checks(form, &g));
issues
}dominates(g, a, b) — страница a лежит на
всех путях от start к b (доминаторы, алгоритм
Cooper–Harvey– Kennedy); для условных вопросов (show-if)
добавляется проверка, что вопрос в условии не скрыт тем же условием
(самоссылка) и не зависит циклически (топологическая сортировка
зависимостей show-if/computed). Результаты
показываются в FM-03 с переходом к узлу; «Симулятор пути» прогоняет
runtime (5.3) с заданными ответами.
5.2 Рендер формы в разных средах
Единая модель → трейт FormRenderer; логика (видимость,
переходы, валидация) — одна реализация в Rust
(meridian-forms::runtime), которая для веба компилируется в
WebAssembly и выполняется в браузере, а на сервере повторно проверяет
присланный ответ (защита от обхода валидации).
| Среда | Рендерер | Особенности |
|---|---|---|
| Десктоп (предпросмотр, киоск, ручной ввод) | QML-компоненты по типам вопросов, стиль MDS + тема формы | тот же runtime через cxx-qt; размеры устройств эмулируются |
| Веб (Meridian Cloud) | серверный HTML-шаблон (без JS работает постранично: каждая страница
— <form method=post>, переходы считает сервер) +
прогрессивное улучшение: WASM-runtime для мгновенной
валидации/логики/автосохранения |
WCAG 2.1 AA, тема → CSS-переменные, CSP строгий, без внешних ресурсов, i18n |
| PDF с полями (AcroForm) | кодек PDF: поля q<id> (text/Tx),
флажки q<id>.<opt> (Btn checkbox),
переключатели — группа радио q<id> с экспортными
значениями = id вариантов, список — Ch, подпись —
Sig-поле (без сертификата — визуальная), дата —
Tx с форматом AFDate_FormatEx;
вычисляемые/условные — не поддерживаются: печатаются все вопросы,
условие отображается подписью «(если …)»; скрытый
form-id/form-version/response-id
в полях _meridian_* (hidden) и QR для сопоставления |
NeedAppearances true, шрифты встроены (кириллица),
PDF/UA по возможности |
| Печать (бумага) | тот же макет без полей: линии/клетки для текста, квадраты для отметки, якорные метки по углам (для будущего распознавания) | через Meridian Print |
| HTML в письме (Mail) | статичная HTML-форма с mailto:-отправкой
(method=post enctype=text/plain) для простых типов; сложные
типы — ссылка на PDF/Cloud |
ограничения почтовых клиентов (без JS) |
5.3 Runtime респондента
state = { page: first_visible_page, answers: {}, visited: [], started: now }
render(page): questions = [q in page.questions | eval(q.show_if, answers) != false], подстановка {qN} в тексты, перемешивание по seed(response_id)
on next:
errors = validate(page.questions_visible, answers) // required, type, regex, min/max, counts, consent
if errors: show, stop
rules = logic.rules.filter(after == page) in order; target = first rule where cond is None or eval(cond) == true
if none: target = next page in order (skipping pages with show_if == false); if none: end
visited.push(page); page = target; if target == end: submit()
on back: page = visited.pop(); answers for pages after it remain (но при повторном прохождении могут перезаписаться)
submit: status = complete; score = grade(); path = visited + [page]; persist (локально / POST на сервер / письмо)
autosave: каждые 5 с при изменениях (localStorage / файл киоска / черновик на сервере при именных ответах)
time-limit: таймер от started; по истечении — автосабмит текущих ответов
5.4 Слияние ответов из разных источников
- Каждый ответ получает
response_id(ULID) в момент старта у рендерера (веб — на сервере, PDF — в скрытом поле при экспорте каждого экземпляра бланка либо генерируется при импорте, киоск/ручной — локально). - При импорте без идентификатора (CSV из Google Forms и т.п.) —
fingerprint = BLAKE3(form_id ‖ respondent_key ‖ submitted_at (с точностью до минуты) ‖ canonical(answers)); совпадение вdedupe→ пропуск. - Один
response_id, разные содержимые (редактирование ответа) — побеждает большийmodified, предыдущая версия сохраняется в пакете источника сsuperseded="true". - Разные версии формы: ответы привязываются к
form-version; вопросы сопоставляются по стабильнымid(переименование текста не ломает), удалённые вопросы остаются в данных и показываются в сводке группой «Удалённые вопросы (версия N)», новые — пусто для старых ответов; варианты — поoption.id; смена типа вопроса — ответы старого типа конвертируются, если возможно (single → multi), иначе помечаются. - Источники пишут в свои файлы
responses/<source>.xml(append-only), поэтому слияние пакета между устройствами — объединение файлов; индекс в профиле пересобирается. - Mail-канал: Mail распознаёт входящие с
.mfr/PDF/HTML-ответом по заголовкуX-Meridian-Form: <form-id>или по QR/скрытому полю PDF и передаёт в Forms через брокер; письмо помечается «обработано».
5.5 Подсчёт баллов
grade(response, form):
total = 0; max = 0
for q in form.questions where q.points:
max += q.points
a = answers[q.id]
match q.kind:
single-choice | dropdown | yes-no: total += q.points if a ∈ correct_ids
multi-choice: C = correct_ids, S = selected
if !q.partial: total += q.points if S == C
else: total += q.points * max(0, (|S ∩ C| − |S \ C|) / |C|) // штраф за лишние / penalty for wrong picks
short-text: norm(a) ∈ { norm(x) | x in accepted_answers } (trim, lower, collapse spaces, ё→е; опц. regex / числовой допуск)
number: |a − correct| ≤ tolerance
likert/matrix: сумма по ячейкам с заданными «правильными» столбцами (P1)
ranking: баллы по совпадению позиций (Spearman-подобно, P2)
long-text/file/signature: ручная проверка (grading.xml) → баллы из неё
passed = total / max * 100 >= pass_percent
5.6 Агрегация для сводки
| Тип | Агрегаты | Диаграмма Chart |
|---|---|---|
| single/multi/dropdown/yes-no | количество и % по варианту (multi — % от респондентов), «другое» — список | горизонтальные столбцы / круг (single) |
| scale/rating/number | среднее, медиана, σ, min/max, гистограмма (bins по шкале или Фридман–Диаконис) | столбцы |
| nps | доли детракторов (0–6), нейтралов (7–8), промоутеров (9–10); NPS = %пром − %детр; динамика по неделям | NPS-шкала + линия |
| likert/matrix | таблица строки × столбцы (кол-во, %), средний балл по строке | тепловая карта / сгруппированные столбцы |
| ranking | средняя позиция, % «первое место» | столбцы |
| date/time | распределение по дням/месяцам/часам | столбцы |
| text | список последних, длина, частые слова (P2 облако), Assist-резюме (P2) | — |
| file | число файлов, типы, общий размер | — |
| тест | распределение баллов, % правильных по вопросу, средний балл, зачёт/незачёт | гистограмма |
Инкрементальность: агрегаты хранятся в памяти как счётчики по
(q, bucket); новый ответ обновляет счётчики за O(число
ответов в нём); фильтр по сегменту — SQL по answers
(WHERE response_id IN (SELECT … WHERE q=? AND v_id=?)) с
пересчётом; при > 100 000 ответов — материализованные агрегаты в
SQLite с инвалидацией по дельте.
5.7 Живая связь с Sheets
«Открыть в Sheets» создаёт .msx с таблицей-«внешним
источником» forms://<form-id>/responses?version=all:
столбцы — response_id, submitted,
channel, respondent, затем по вопросу (multi —
через ; и дополнительные булевы столбцы по вариантам,
matrix — столбец на строку); обновление — по событию от брокера Hub
(forms.responses.changed) или по кнопке «Обновить»;
формулы/сводные/диаграммы пользователя не трогаются (таблица растёт
вниз, структурированные ссылки по имени столбца).
5.8 MeridianScript API
interface FormsApp {
form: Form | null; // активная форма / active form
open(path: string): Promise<Form>; create(title: string, opts?: { template?: string }): Promise<Form>;
on(event: 'responseReceived' | 'formPublished' | 'formClosed', h: (e: { form: Form; response?: Response }) => void): Disposable;
}
interface Form {
id: string; title: string; version: number; pages: Page[]; questions: Question[]; logic: Logic; settings: Settings; theme: Theme; languages: string[];
addPage(title?: string, at?: number): Page; question(id: string): Question | null;
validate(): LogicIssue[]; preview(device?: 'desktop'|'tablet'|'phone'): void;
publish(opts?: { channel: 'cloud'; close?: Date }): Promise<{ url: string; qr: Blob }>; unpublish(): Promise<void>;
exportPdf(path: string, opts?: { fillable?: boolean; lang?: string }): Promise<void>;
responses: Responses; translations: Translations; save(): Promise<void>;
}
interface Page { id: string; title: string; questions: Question[]; addQuestion(type: QuestionType, text: string, opts?: Partial<QuestionProps>): Question; }
interface Question { id: string; type: QuestionType; text: string; required: boolean; options: Option[]; showIf: string | null; points: number | null;
addOption(text: string, opts?: { correct?: boolean }): Option; remove(): void; moveTo(page: Page, index?: number): void; }
interface Logic { rules: JumpRule[]; addRule(after: Page, target: Page | 'end', condition?: string): JumpRule; computed: Computed[]; }
interface Responses {
count: number; list(filter?: { since?: Date; channel?: string; where?: string }): Promise<Response[]>;
summary(questionId: string): Promise<Summary>; import(path: string): Promise<ImportReport>;
export(path: string, format: 'xlsx'|'csv'|'msx'|'json'|'pdf'): Promise<void>; openInSheets(): Promise<void>;
add(answers: Record<string, unknown>, opts?: { channel?: 'manual'; respondent?: string }): Promise<Response>;
grade(responseId: string, questionId: string, points: number, comment?: string): Promise<void>;
}
interface Response { id: string; submitted: Date; channel: string; respondent: string | null; answers: Record<string, unknown>; score: number | null; passed: boolean | null; }Примеры:
// 1. Письмо с благодарностью и результатом теста после каждого ответа / Thank-you mail with test result
App.on('responseReceived', async ({ form, response }) => {
if (!response?.respondent?.startsWith('email:')) return;
await Meridian.mail.send({ to: response.respondent.slice(6), subject: `Результат: ${form.title}`,
body: `Ваш балл: ${response.score} (${response.passed ? 'зачёт' : 'незачёт'})` });
});
// 2. Собрать форму из списка вопросов в Sheets / Build a form from a Sheets range
const rows = await Meridian.sheets.active!.range('Вопросы!A2:C50').values();
const f = await App.create('Опрос из таблицы');
const p = f.addPage('Вопросы');
for (const [text, type, opts] of rows.filter(r => r[0])) {
const q = p.addQuestion(type as QuestionType, String(text), { required: true });
for (const o of String(opts ?? '').split('|').filter(Boolean)) q.addOption(o);
}
const issues = f.validate(); if (issues.length) await Meridian.ui.message(issues.map(i => i.message).join('\n'));
await f.save();
// 3. Еженедельный экспорт ответов в Sheets-файл / Weekly export to a Sheets file
await App.form!.responses.export('~/Отчёты/ответы.msx', 'msx');На сервере (Cloud) скрипты формы не выполняются: серверная валидация
и логика — только декларативные (validation,
logic), что гарантирует одинаковое поведение и
безопасность.
6. Форматы
| Формат | Чтение | Запись | Особенности |
|---|---|---|---|
.mfx / .mft |
да | да | раздел 4; ответы внутри пакета по источникам; версии формы в
history/ |
.mfr (пакет ответов) |
да | да | обмен ответами между каналами и устройствами;
manifest.xml ссылается на форму и версию |
| PDF (AcroForm) | да (заполненные бланки → ответы, через PDF-кодек: чтение значений
полей, в т.ч. с NeedAppearances; сканы — P2) |
да | раздел 5.2; шрифты встроены; QR с
form-id/version/response-id |
XLSX / CSV / .msx |
да (импорт ответов: сопоставление столбцов по заголовкам/ID; экспорт Google Forms/MS Forms/Яндекс Форм распознаётся по шаблонам заголовков) | да | экспорт: один лист «Ответы» + лист «Вопросы» (словарь id → текст) + лист «Сводка» |
| JSON | да | да | ответы как массив объектов
({ response_id, submitted, answers: { q1: … } }), форма —
JSON-проекция form.xml для API |
.docx / .xlsx (импорт вопросов) |
да | — | нумерованные абзацы → вопросы, вложенные списки/a) →
варианты, таблицы → матрицы; типы угадываются по форме («Да/Нет», «от 1
до 5») с подтверждением |
| HTML (встраивание, письмо) | — | да | статичная форма (письмо) и сниппет встраивания (Cloud) |
| XLIFF 2.0 / XLSX переводов | да | да | ключи = id элементов |
7. Горячие клавиши
| Действие | Клавиши |
|---|---|
| Новая форма / открыть / сохранить | Ctrl+N / Ctrl+O / Ctrl+S |
| Добавить вопрос (после текущего) / добавить страницу / раздел | Ctrl+Enter / Ctrl+Shift+Enter /
Ctrl+Shift+H |
| Дублировать вопрос / удалить | Ctrl+D / Delete |
| Переместить вопрос вверх/вниз / на предыдущую/следующую страницу | Alt+↑/Alt+↓ /
Alt+Shift+↑/Alt+Shift+↓ |
| Обязательный вопрос (переключить) | Ctrl+Shift+R |
| Тип вопроса (галерея) | Ctrl+T |
| Добавить вариант / «другое» | Enter в варианте / Ctrl+Shift+O |
| Режимы: Вопросы / Логика / Оформление / Предпросмотр / Ответы | Ctrl+1 / Ctrl+2 / Ctrl+3 /
Ctrl+4 / Ctrl+5 |
| Предпросмотр: устройство ПК/планшет/телефон | Ctrl+Alt+1/2/3 |
| Проверить логику | F7 |
| Опубликовать / обновить публикацию | Ctrl+Shift+P |
| Экспорт PDF / печать | Ctrl+Shift+E / Ctrl+P |
| Открыть ответы в Sheets | Ctrl+Shift+S |
| Импорт ответов | Ctrl+Shift+I |
| Командная палитра | Ctrl+K |
| Отмена / повтор | Ctrl+Z / Ctrl+Y |
| Респондент (веб/предпросмотр): следующая/предыдущая страница, отправить | Enter (в последнем поле) / Alt+← /
Ctrl+Enter |
8. Критерии готовности
8.1 Forms 1.0 (P0 закрыты; Cloud-канал — фаза 6)
- Все типы вопросов P0 работают в десктоп-предпросмотре, печати и PDF (AcroForm читается в Acrobat Reader, Preview, Firefox, Chrome; заполненный в них бланк импортируется с 100 % значений — тест на 20 формах).
- Логика: 50 тестовых форм с ветвлением — проверка находит все заложенные циклы/недостижимые страницы (0 ложных срабатываний на корректных формах); runtime проходит 500 сценариев ответов с ожидаемыми маршрутами.
- Канал Mail: рассылка 100 адресатам через Mail; ответы (PDF и HTML) автоматически попадают в форму; дубликаты не создаются при повторной обработке.
- Импорт ответов из экспорта Google Forms/MS Forms (CSV/XLSX) — сопоставление ≥ 95 % столбцов автоматически.
- Сводка на 100 000 ответов: открытие ≤ 2 с, фильтр ≤ 1 с; диаграммы Chart всех типов из таблицы 5.6.
- Тесты: подсчёт баллов совпадает с эталоном на 200 ответах (включая частичные), ручная проверка открытых.
- Живая связь с Sheets: новый ответ появляется в открытом
.msx≤ 5 с. - Многоязычная форма (ru/en/kk) — экспорт/импорт XLIFF без потерь; переключение языка в предпросмотре.
- Печать формы и ответа — снимочные тесты 30 форм.
- MeridianScript: 3 примера работают;
validate()возвращает те же проблемы, что FM-03. - Доступность предпросмотра и веб-рендера (аудит axe на 20 формах — 0 критичных).
8.2 Forms 1.x
Cloud-канал (ссылка, QR, встраивание, один ответ, SSO организации, уведомления), файлы респондентов, ранжирование, подпись, вычисляемые поля, киоск, автосохранение ответа, скрытые поля, квоты (P2), банк вопросов (P2), распознавание бумажных анкет (P2).
9. Риски
| Риск | Вероятность | Влияние | Снижение |
|---|---|---|---|
| Без Cloud (фаза 6) главный сценарий «ссылка на опрос» недоступен в 1.0 — продукт выглядит неполным | высокая | слабый приём Forms 1.0 | честно позиционировать 1.0 как «формы для рассылки, PDF и киоска»;
ранний запуск минимального Cloud-эндпойнта только для форм (отдельный
маленький сервис forms-service, вопрос 10.1) |
| AcroForm-совместимость просмотрщиков (Preview на macOS теряет значения, мобильные просмотрщики не сохраняют) | средняя | потеря ответов | NeedAppearances, тесты в матрице просмотрщиков,
подсказка респонденту «сохраните и отправьте файл»,
HTML-альтернатива |
Почтовые клиенты не отправляют HTML-формы (mailto: с
text/plain) единообразно |
высокая | HTML-канал ненадёжен | HTML-форма — только для простых опросов, по умолчанию PDF или ссылка |
| Ветвление + перемешивание + многоязычность → сложные комбинации, ошибки runtime | средняя | неверные маршруты | единый runtime на Rust для всех сред, property-based тесты |
| Личные данные респондентов (152-ФЗ/GDPR): IP, e-mail, подписи | средняя | юридические риски | анонимность по умолчанию, не хранить IP/UA, экспорт/удаление данных респондента, регион хранения в Cloud |
| Сводка на больших объёмах открытых ответов | низкая | медленно | материализованные агрегаты, пагинация текстов |
| Распознавание бумажных анкет (P2) требует OMR и чистых сканов | средняя | низкая точность | якоря и квадраты в бланке, ручная проверка неуверенных |
10. Вопросы к библии
- Сбор ответов до фазы 6. Предлагаем выделить
минимальный
forms-service(приём ответов, рендер веб-формы) в составеcloud-service, но запускаемый раньше — в конце фазы 5, иначе Forms 1.0 выходит без главного канала. Требует правки таблицы фаз (раздел 7) и 9.4. - Редакции. Таблица 9.1: Home — «Forms (личные формы)». Предлагаем определить: Home — все каналы без Cloud; Pro — Cloud-канал; Business — ответы с SSO организации и корпоративные темы.
- Пакет ответов
.mfr— новое расширение, не указанное в таблице 3.2; просим добавить (MIMEapplication/vnd.meridian.forms-responses). - Внешние источники данных Sheets
(
forms://) — механизм должен быть описан в документе Sheets; здесь предполагается, что.msxподдерживает таблицы с внешним источником и обновлением по событию брокера.