Спросите помощника
0/400
Вопрос обрабатывается ИИ-сервисом и сохраняется для улучшения ответов. Не вводите персональные данные.

    project.json — манифест проекта

    Статус: проектная спецификация (draft). Проект только начат — схема намеренно минимальна. Версионирование схемы пока неявное (v1): поле schemaVersion в файл не пишется, его отсутствие означает v1. Оно появится, только когда возникнет несовместимое изменение.

    project.json — декларативное описание REEEL-проекта и набора его собираемых конфигураций . Это build-time источник истины : из него редактор строит список «вариантов сборки», а build-worker понимает, что и как собирать. Идентичность проекта — это его директория ; отдельного поля name у проекта нет.

    Необязательный title — это ПОДПИСЬ, а не идентичность: как проект называют люди. Им ничего не адресуется, он не обязан быть уникальным и его можно менять в любой момент. Нужен там, где сказанное иначе теряется: человек просит «Список задач», каталог получает task_list , и слово не сохраняется нигде.

    Принципы

    • Декларативность. Манифест описывает что собирать, а не как . Конкретные шаги сборки остаются за config_builder / make build .
    • Источник истины. Поля сборки (платформа, точка входа, плагины и т.д.) живут здесь, а не в глобальных configs/ . Направление развития — перенести configs/<profile>/config.json в конфигурации проекта (см. ниже, миграция не завершена).
    • Плоская и явная модель контента. Точка входа ( entry ) — это явный путь к файлу относительно корня проекта. Каталог этого файла становится корнем контента; все вложенные ссылки ( AppRoute /home , import components/theme ) резолвятся относительно него. Слои platform / design / common в построении пути не участвуют .
    • Минимализм. Никаких полей «на вырост». Версия схемы неявная (v1) и в файл не пишется; name у проекта нет (идентичность = директория). title исключением не является: он появился не «на вырост», а потому что имя, названное человеком при создании, было некуда положить.

    Схема верхнего уровня

    {
      "version": "1.0.0",
      "entry": "reeel/main.reeel",
      "configurations": [
        {
          "name": "reeel-vod_ios",
          "target": "ios",
          "settings": {
            "bundleId": "su.flutter.reeel.vod",
            "displayName": "Reeel VOD"
          }
        },
        {
          "name": "reeel-vod_web",
          "target": "web",
          "entry": "reeel/landing.reeel",
          "settings": {
            "displayName": "Reeel VOD"
          }
        }
      ]
    }
    Поле Тип Описание
    title string Как проект называют люди — ПОДПИСЬ, не идентичность: ничем не адресуется, уникальности не требует, меняется свободно. Пустая строка равна отсутствию. Необязательное.
    version string Версия проекта (semver), общая для конфигураций. Необязательное.
    entry string Точка входа по умолчанию для всех конфигураций — путь к .reeel -файлу относительно корня проекта (напр. reeel/main.reeel ). По умолчанию reeel/main.reeel .
    theme object | string Файлы темы по умолчанию: { "light": "data/theme/light.json", "dark": "data/theme/dark.json" } или одна строка (один файл на обе). Необязательное — без него файлы ищутся по конвенции data/theme/{light,dark}.json . Переопределяется в конфигурации.
    locales object | string Локализация по умолчанию: { "default": "ru" } (фолбэк-локаль, когда системная недоступна) или одна строка-локаль. Список файлов локалей обнаруживается из data/strings/* . Переопределяется в конфигурации.
    designWidth number Логическая ширина, в которой свёрстан дизайн. Когда задана — весь UI раскладывается в этой ширине и равномерно масштабируется под экран (resolution-independent дизайн, напр. TV-вёрстка под 1920, обязанная выглядеть 1:1 независимо от разрешения). Без неё — адаптивная раскладка по логическому размеру устройства. Используется в превью редактора (перекрывает выбранное устройство) и при сборке зеркалится в design_width билд-конфига. Необязательное.
    docs array Пути (файлы или каталоги) с документацией проекта, которую читает движок знаний. По умолчанию ["docs"] . Каталог даёт все .md внутри рекурсивно, файл — сам себя; шаблонов и масок нет. Несуществующий путь — не ошибка. Необязательное.
    domains array Собственные домены сайта — имена, на которых он отвечает помимо адреса по слагу ( reeel.dev , shop.acme.com ). См. Собственные домены . Необязательное.
    git object Репозиторий проекта и правила работы с ним — см. Секция `git` . Появляется, когда редактор заводит в проекте git; секретов не содержит. Необязательное.
    configurations array Список собираемых конфигураций. Минимум одна.
    schemaVersion и name намеренно отсутствуют (см. «Принципы»). Если в файле встретится schemaVersion , инструмент его прочитает; при записи v1 не эмитится.

    Секция git

    Редактор хранит версии проекта во встроенном git (ставить git на компьютер не нужно). Секция говорит, где репозиторий и по каким правилам с ним работать; её пишет редактор, но править руками можно.

    "git": {
      "remote": "https://github.com/acme/app.git",
      "defaultBranch": "main",
      "policy": {
        "flow": "trunk",
        "integration": "rebase",
        "featurePrefix": "feature/",
        "requireUpToDateBeforePush": true,
        "autoCommitOnCloudPublish": false,
        "protectMain": true,
        "deleteBranchOnFinish": true,
        "pushOnCommit": true
      }
    }
    Поле Тип Описание
    remote string Адрес репозитория ( https://… или git@host:… ). Нужен, чтобы проект, скачанный из облака на другой компьютер, предложил подключиться к своему репозиторию. Токены и ключи сюда не попадают никогда : токен живёт в системном хранилище паролей, ключ — в ~/.reeel/ssh . Необязательное.
    defaultBranch string Главная ветка. По умолчанию main .
    policy.flow string Схема веток. Сегодня единственная — trunk : одна главная ветка и короткие ветки-фичи от неё.
    policy.integration rebase | merge | squash Как завершённая ветка попадает на главную: rebase — версии встают в линию поверх главной; merge — одним слиянием, ветка остаётся видна в истории; squash — одной версией (пока движок выполняет его как merge ). По умолчанию rebase . Так же объединяется и то, что приносит «Обновить»: rebase или merge .
    policy.featurePrefix string С чего начинается имя ветки-фичи: feature/login . По умолчанию feature/ . Ветка с таким префиксом получает кнопку «Завершить».
    policy.requireUpToDateBeforePush bool Отправка сначала сверяется с репозиторием и отказывает, пока в нём есть версии, которых нет здесь. По умолчанию true .
    policy.autoCommitOnCloudPublish bool Публикация в облако заодно сохраняет версию, если есть изменения. По умолчанию false .
    policy.protectMain bool Когда подключён репозиторий, версия прямо на главной ветке не сохраняется — редактор предлагает ветку. Без репозитория правило ничего не значит: одиночный проект не дёргают. По умолчанию true .
    policy.deleteBranchOnFinish bool Завершённая ветка удаляется — и здесь, и в репозитории, если в его копии нет версий, которых нет на этой машине. Выключите, если ветки в проекте живут дольше влития (их читают на ревью): тогда «Завершить» только переносит версии. По умолчанию true .
    policy.pushOnCommit bool «Сохранить версию» сразу отправляет её в репозиторий, если он подключён и в нём нет версий, которых нет здесь. Отказ (нет сети, истёк токен) версию не отменяет: окно говорит, что она сохранена, но осталась на этом компьютере, и «Отправить» появляется отдельной кнопкой. По умолчанию true .

    Секция читается снисходительно: неизвестное значение — значение по умолчанию, а не ошибка. Пишется целиком, чтобы читающий файл видел правило, не зная умолчаний.

    .git в облачный снапшот не попадает (как и всё, что начинается с точки), и облако git не заменяет: облако доставляет интерфейс в приложение, git хранит историю и ветки.

    Собственные домены ( domains )

    {
      "domains": ["reeel.dev", "www.reeel.dev"],
      "configurations": [
        { "name": "site_web", "target": "web", "settings": { "origin": "https://reeel.dev" } }
      ]
    }

    Это заявка, а не владение. Имя в списке само по себе не даёт ни маршрута, ни сертификата. Раздача каждый цикл заново выводит, законно ли имя, из DNS: в зоне домена должна стоять запись:

    _reeel-verify.<имя>   TXT   reeel-verify=<id владельца проекта>

    Нет записи, не та запись, запись убрали — имя просто не приезжает, и сайт живёт на своём адресе по слагу. Отдельной «отвязки» не существует: убрали запись или убрали строку из domains[] — на следующем цикле имени не станет. Поэтому один и тот же домен может стоять в domains[] у нескольких проектов: подтвердит его тот, у кого есть доступ к DNS.

    Имя приводится к одному виду — нижний регистр, без завершающей точки, дубликаты отбрасываются: дальше значение сверяется с заголовком Host побайтово, и два написания одного имени не должны стать двумя сайтами.

    Чего в списке быть не может:

    почему
    *.example.com заявка на целое поддерево требует другого доказательства и другого сертификата — перечислите имена
    https://example.com здесь голое имя; URL — это settings.origin соседней конфигурации
    example.com/page , example.com:8080 путь и порт частью имени хоста не бывают
    localhost , example одна метка — это хост в чьей-то локальной сети, не сайт в интернете
    пример.рф объявите форму xn--… : преобразование не угадывается, а ошибка публикует имя, ведущее в другое место

    settings.origin обязан быть среди объявленных имён. Из него собираются canonical , og:url и sitemap ; если он указывает на имя, которого сайту не давали, целый домен страниц канонизируется на чужой адрес. Проверка манифеста предупреждает об этом — но только когда домен объявлен: сайт на адресе по слагу тоже задаёт origin , и сравнивать там не с чем.

    Он же — основной домен: остальные уходят на него редиректом. Как только хост origin реально обслуживается (TXT подтверждён, A указывает на раздачу), запрос на любой другой подключённый домен получает 301 на тот же путь и ту же строку запроса основного. Доменов может быть сколько угодно — уходят все: только редирект передаёт основному имени вес ссылок, поставленных на www. или на старый адрес; canonical и noindex этого не делают.

    Куда пришёл запрос Ответ
    хост origin 200
    другой подключённый домен 301 на origin + путь + строка запроса, Cache-Control: public, max-age=3600
    адрес по слагу <slug>.reeel.site 200 и X-Robots-Tag: noindex ; с "redirectSlug": true — тоже 301
    /.well-known/… на любом имени не редиректится никогда
    origin не задан или его хост ещё не обслуживается редиректов нет: все имена отвечают 200

    settings.redirectSlug включается отдельно, потому что адрес по слагу — вход на сайт до подключения домена и запасной после. Без origin настройка не делает ничего, и проверка манифеста об этом предупреждает.

    Куда домен ведёт в DNS — запись A на адрес раздачи — говорит консоль проекта; сертификат выпускается сам при первом обращении по имени.

    Схема одной конфигурации

    Поле Тип / Значения Описание
    name string Уникальное имя варианта сборки (показывается в выпадающем списке редактора). Обязательное. Конвенция — <имя_директории_проекта>_<target> (напр. reeel-vod_ios ); редактор/шаблоны проставляют его автоматически при создании.
    target ios | android | macos | windows | linux | web | tizen | … Целевая платформа сборки. Обязательное. Список расширяемый.
    entry string Необязательный override точки входа для конкретной конфигурации. Если не задан — берётся top-level entry манифеста.
    theme object | string Необязательный override темы (формат как у top-level theme ). Если не задан — берётся top-level theme , иначе обнаружение.
    locales object | string Необязательный override локализации (формат как у top-level locales ). Если не задан — берётся top-level locales .
    pages object Только kind: website . Адреса страниц: {"o-klube": "about"} — ключ — это URL, значение — имя .reeel -файла в reeel/ . Без него адрес страницы — имя файла, каким его назвал автор. См. Адреса страниц .
    settings object Платформо-специфичные обязательные/опциональные поля (см. таблицу-заготовку).

    Адреса страниц ( pages )

    Только для веб-конфигурации. Без этого поля URL страницы — это имя файла: reeel/idx.reeel живёт по /idx и никак иначе.

    { "name": "site_web", "target": "web", "kind": "website",
      "pages": { "o-klube": "about", "nashi-partnery": "sponsors_2024" } }

    Объявлено в конфигурации , а не на верхнем уровне: адрес — свойство того, как проект публикуется , а не самого проекта. Один и тот же файл бывает экраном приложения и страницей сайта, и URL есть только у второго.

    Правила:

    • Имя файла продолжает работать. Алиас добавляет адрес, а не отбирает — ничто, что уже ссылалось на страницу, не ломается.
    • Сайт рекламирует адрес, а не файл. hreflang , canonical и статический экспорт печатают алиас; при заходе по имени файла страница всё равно указывает канонический адрес. Иначе сайт публиковал бы URL, по которым сам не отвечает.
    • Ссылки в разметке можно писать на имя файла — в выдачу они пойдут по адресу, так что посетитель видит имя, выбранное сайтом.
    • Два запрета, проверяемых при загрузке проекта: адрес не может совпадать с кодом локали (спрятал бы целый язык) и с именем другой страницы (спрятал бы её). Такая запись отбрасывается, а причина пишется в лог инстанса.

    Точка входа ( entry )

    • Явный путь к файлу от корня проекта (напр. reeel/main.reeel ), а не имя файла + автопоиск по слоям.
    • По умолчанию reeel/main.reeel для новых проектов.
    • Задаётся на уровне манифеста ( entry ) как дефолт для всех конфигураций и переопределяется в конкретной конфигурации ( configuration.entry ).
    • Корень контента = каталог точки входа. Для entry: "reeel/main.reeel" корень контента — reeel/ , стартовый view — main . Все маршруты и импорты внутри файлов резолвятся относительно этого корня; platform / design / common в построении пути не участвуют.

    Данные, тема и локализация ( data/ )

    Данные оформления живут в каталоге data/ рядом с reeel/ : data/theme/<mode>.json наполняет ambient $theme.* , data/strings/<locale>.arb — $strings.* . Для сайтов ( kind: website ) там же лежит data/web/ : scripts/*.js для элемента Script и settings.scripts , html/*.html для HtmlEmbed — синкается и отдаётся общим механизмом /data/<path> , отдельного правила нет.

    Манифест может явно объявить файлы темы ( theme ) и фолбэк-локаль ( locales ) — на верхнем уровне как дефолт и с override в configuration (как entry ). Если поля нет, файлы ищутся по конвенции ( data/theme/{light,dark}.json , data/strings/* ). Подробно — Тема и локализация и структура проекта .

    Системные тема/язык. Без своего переключателя проект следует за системой: тема — system (светлая/тёмная по настройке ОС), язык — лучшая из доступных локалей по приоритету ОС (фолбэк — locales.default ). В редакторе превью симулирует системные настройки тогглом ☀️/☾ и селектором языка рядом с тогглом ориентации; на устройстве берётся реальная настройка ОС. Явный theme.set(...) / strings.set(...) в разметке перекрывает системный выбор.

    Обязательные поля по платформам ( settings )

    Заготовка. Точный набор уточнят backend / worker при реализации сборок — помечайте как расширяемое , не закрепляйте лишнего раньше времени.

    target Ожидаемые поля settings (черновик)
    ios bundleId , displayName , иконки, профиль подписи — уточнит worker
    android applicationId , displayName , иконки, keystore — уточнит worker
    macos bundleId , displayName , иконки, подпись — уточнит worker
    windows appId , displayName , иконки — уточнит worker
    linux appId , displayName , иконки — уточнит worker
    web (PWA) displayName , base href, манифест PWA — уточнит worker
    tizen appId , displayName , профиль подписи Tizen — уточнит worker

    Поля settings по виду проекта ( kind )

    settings — сырая карта внутри КОНФИГУРАЦИИ, и смысл её ключей задаёт kind , объявленный рядом. Пространств имён у ключей нет и не нужно: у конфигурации ровно один вид, поэтому настройки фильма и настройки приложения не встречаются в одной карте — они живут в разных конфигурациях одного проекта. Неизвестные ключи переживают загрузку и сохранение, так что чужое ничего не ломает.

    Имена ключей выбираются так, чтобы читаться БЕЗ взгляда на kind : frameSize у фильма и (в будущем) pageSize у документа лучше, чем два разных size .

    kind Ключ Тип Значение
    app bundleId / applicationId string Идентификатор установки; без него упакованная сборка отвергается
    app displayName / appName string Имя приложения, видимое человеку
    website origin string Абсолютный адрес публикации ( https://reeel.dev ); относительный отвергается, а не чинится
    website redirectSlug bool true — адрес по слагу тоже отвечает 301 на origin . По умолчанию он отвечает сам и закрыт от индексации; подключённые домены уходят на origin без этой настройки — см. Собственные домены
    website scripts array Авторские скрипты на весь сайт: [{ "src"|"url", "where", "async", "defer", "requiresConsent", "id" }] . Выполняются раньше страничных Script и только на подключённом домене — см. Скрипты сайта
    motion frameSize [w, h] Кадр, в который отдаёт эта конфигурация. Прежнее имя size продолжает работать
    motion fps number Кадров в секунду на выводе
    motion codec "h264" | "hevc" Чем кодировать. h264 — то, что площадки рекомендуют сами (YouTube: «H.264, High Profile»), и файл они всё равно перекодируют у себя. hevc вдвое легче, но рекомендацией не является
    motion quality "small" | "normal" | "high" Насколько подробно писать — это только битрейт. normal — то, что площадки советуют для загрузки; high имеет смысл, если файл пойдёт в монтаж

    Скрипты сайта ( settings.scripts )

    Только kind: website . Авторские скрипты уровня САЙТА — те, что одинаковы на всех страницах, — объявляются один раз здесь, а не элементом Script на каждой странице; эмитятся они раньше страничных:

    { "name": "site_web", "target": "web", "kind": "website",
      "settings": {
        "origin": "https://reeel.dev",
        "scripts": [
          { "src": "data/web/scripts/site.js", "defer": true },
          { "src": "https://plausible.io/js/script.js", "defer": true,
            "requiresConsent": "analytics",
            "attrs": { "data-domain": "reeel.dev" } }
        ]
      } }
    Поле Описание
    src Адрес скрипта: файл проекта полным путём от корня ( data/web/scripts/… ) или внешний https:// -адрес
    async , defer Одноимённые атрибуты тега
    requiresConsent "analytics" или "marketing" : тег инертен до согласия посетителя на категорию
    id Атрибут id тега
    attrs Прочие атрибуты тега объектом: data-* , type (только "module" ), integrity , crossorigin

    Все теги встают в конец тела страницы. Запись с полем вне этой таблицы — в том числе с ушедшими url и where — пропускается с предупреждением.

    Исполняются авторские скрипты на адресах самого сайта : на адресе по слагу <slug>.reeel.site и на подключённом домене — хосте из domains или хосте settings.origin . Под корнем платформы ( *.app.reeel.dev , *.sys.reeel.dev , прежняя зона сайтов *.in.reeel.dev ) теги в HTML не эмитятся вовсе. Элементы `Script` и `HtmlEmbed` и категории `Consent` работают по тому же правилу.

    Аналитика ( settings.analytics )

    Только kind: website . Счётчики объявляются : сниппет печатает платформа, ни элемента Script , ни файла с JavaScript для этого не нужно.

    "settings": {
      "origin": "https://reeel.dev",
      "analytics": {
        "yandexMetrika": { "id": 12345678, "webvisor": true },
        "googleAnalytics": { "id": "G-XXXXXXX" }
      }
    }
    Счётчик Поля
    yandexMetrika id — номер счётчика; флаги сниппета webvisor (по умолчанию false ), clickmap , trackLinks , accurateTrackBounce (по умолчанию true ); ecommerce — электронная коммерция (по умолчанию false ; true или имя контейнера данных, "dataLayer" )
    googleAnalytics id — идентификатор потока GA4 вида G-…

    Значение счётчика — всегда объект, не голое число: так в него добавляются поля. Неизвестный счётчик, неизвестное поле или id не той формы — предупреждение, счётчик пропускается.

    • Объявленный счётчик всегда ждёт согласия посетителя на категорию analytics ; снять метку нельзя.
    • Работает там же, где авторские скрипты: <slug>.reeel.site , подключённый домен, превью редактора, статический экспорт.
    • Метрика ставит cookie на корневой домен ( .example.com ), и настройки для этого у неё нет — поэтому счётчик на example.com видит и shop.example.com тем же посетителем. Статистике, которая важна, место на подключённом домене.
    • Остальное из кода, который выдаёт Метрика, платформа печатает сама: адрес счётчика и технические поля ( ssr , referrer , url ). Пикселя <noscript> нет намеренно: он посчитал бы посетителя, которого не спросили.
    • Цели отправляет разметка: analytics.goal("order_sent") , с параметрами — analytics.goal("order_sent", mapOf("price", 1200, "plan", "pro")) . В Метрику это уходит как reachGoal , в Google Analytics — событием с тем же именем.
    • Google Tag Manager сюда не входит: контейнер сам загружает произвольный код, то есть это авторский скрипт — элемент Script .

    Капча ( settings.captcha )

    Проверка «я не бот» для форм сайта объявляется так же, как счётчики:

    "settings": {
      "origin": "https://reeel.dev",
      "captcha": {
        "yandexSmartCaptcha": { "siteKey": "ysc1_…" }
      }
    }
    Сервис Поля
    yandexSmartCaptcha siteKey — ключ клиентской части ( ysc1_… ); invisible (по умолчанию false ) — виджета на странице нет, проверка запускается в момент отправки
    • Разметка сервиса не называет: элемент `Captcha` читает это объявление. Поэтому у другой конфигурации того же сайта может быть другой сервис, а страницы останутся теми же.
    • Сюда идёт только клиентский ключ. Серверный ( ysc2_… ) — секрет сервера, который проверяет токен. project.json синкается на платформу и виден всем участникам проекта; серверный ключ или поле serverKey в нём отвергаются с предупреждением, капча при этом не подключается.
    • Неизвестный сервис, неизвестное поле, ключ не той формы — предупреждение, капчи нет; форма с Captcha увидит отказ ( failed ).

    Кадр у моушна: зачем он в проекте, а не только в сцене

    Формат доставки принадлежит проекту. Один вариант уходит в горизонт YouTube, другой — в вертикаль Shorts, и это две КОНФИГУРАЦИИ одного проекта с общими компонентами, а не два проекта:

    {
      "entry": "reeel/main.reeel",
      "configurations": [
        { "name": "youtube", "kind": "motion", "target": "macos",
          "entry": "reeel/youtube.reeel",
          "settings": { "fps": 30, "frameSize": [1920, 1080] } },
        { "name": "shorts", "kind": "motion", "target": "macos",
          "entry": "reeel/shorts.reeel",
          "settings": { "fps": 30, "frameSize": [1080, 1920] } }
      ]
    }

    Общее — компоненты, ассеты, тема, строки — лежит в корне один раз: импорт считается от каталога файла, а начинающийся со / — от корня проекта, поэтому все корневые .reeel видят один пул.

    Кто кого перебивает. *Scene size: важнее settings.frameSize : проект говорит, во что отдаётся ВСЁ, сцена вправе отличаться. Сцена, не объявившая ничего, берёт кадр у конфигурации — и только так фильм перестаёт зависеть от того, как растянуто окно редактора. Превью при этом показывается в том же кадре: верстать в форме окна, а отдавать в форме кадра — значит узнать о расхождении на готовом файле.

    Конфигурация kind: motion без frameSize — предупреждение проверки, а не отказ: сцена может объявить кадр сама, и такой проект работает.

    Кадры под площадки

    Числа, которые площадки принимают сегодня. Одна строка — один КАДР; площадки перечислены внутри, потому что совпавшие числа — это не два формата, а два имени одного и того же. Частота 30 берётся везде; 24 остаётся выбором автора.

    frameSize площадки
    [1080, 1920] YouTube Shorts, Instagram Reels, TikTok, VK Клипы, Facebook Reels, Snapchat
    [1920, 1080] YouTube, VK Видео, Vimeo, Rutube
    [3840, 2160] YouTube 4K, Vimeo 4K
    [1080, 1080] Instagram, Facebook, LinkedIn
    [1080, 1350] Instagram, Facebook — 4:5, самый высокий кадр, который лента показывает целиком

    Тот же список живёт в коде — reeel_project/lib/src/motion_formats.dart , — и экран вывода показывает его как выбор формата ВЫВОДА.

    Сверено с первоисточниками 26.09.2026. Кадры и частоты — из рекомендаций YouTube для загрузок и справки Instagram по Reels и по разрешению в ленте : лента Instagram показывает целиком всё в пределах от 1.91:1 до 4:5 (это и есть 1080×1350), Reels просит 9:16 и не меньше 30 кадров, YouTube называет 16:9, частоты 24/25/30/48/50/60 и кодек H.264.

    Добавить проекту вариант под площадку — работа агента, а не экрана вывода. Экран за «молотком» ничего в project.json не пишет: он выводит то, что уже объявлено, и умеет вписать фильм в чужой кадр (целиком, с полями по краям). Новый кадр — это новая конфигурация, а её появление меняет проект, и решение об этом принимает тот, кто проект ведёт.

    Агента поэтому просят прямо: «добавь вариант под Shorts» или «сделай этот проект горизонтальным». Он дописывает или ЗАМЕНЯЕТ конфигурацию:

    { "name": "shorts", "kind": "motion", "target": "macos",
      "entry": "reeel/shorts.reeel",
      "settings": { "fps": 30, "frameSize": [1080, 1920] } }

    Своя точка входа у варианта не обязательна, но обычно нужна: вёрстка сделана под кадр, и вертикаль, показанная в горизонтальном кадре, ляжет с полями, а не перестроится. Вписывание — честное поведение вывода, а не замена вёрстке.

    Связь с config.json

    project.json — build-time источник истины проекта. config.json (текущий формат из ../frontend/configuration.md) при этом не исчезает автоматически : рассматривается как потенциальный runtime-артефакт, выдаваемый нашим сервером — например, server_url нашего сервера и иные данные, которых нет и не должно быть в пользовательском project.json .

    Поле entry присутствует в обоих форматах и имеет одинаковый смысл — явный путь к точке входа относительно корня, содержащего reeel/ (в config.json — относительно профиля assets/<profile>/ ). Рантайм сам берёт dirname / basename для определения корня контента и стартового view.

    Таблица соответствия (направление миграции, не завершено ):

    Поле configs/*/config.json Куда переходит Примечание
    platform → configuration.target Платформа сборки
    design остаётся за config.json Тип оболочки приложения (isMobile/isTV/isDesktop, CommonAppWidget ); это про UI-шелл, а не про источник reeel — в project.json его нет
    entry ↔ entry / configuration.entry Точка входа; одинаковый формат в обоих файлах
    assets → проект сам по себе Проект и есть профиль контента ( ./reeel/ ); отдельное поле не нужно
    web_assets → configuration.settings Профиль ассетов для встроенного локального сервера (отладка)
    local_server → configuration.settings Флаг встроенного локального сервера (отладка)
    reeel_only_assets_fallback → configuration.settings Бандл без сетевого фолбэка
    plugins → configuration.settings.plugins Список пакетов плагинов
    server_url остаётся за сервером Runtime-хост выдачи контента; кандидат на runtime config.json . Отдельно от platform_api_url ( api.reeel.dev ) — адреса платформы для sync/сборки в редакторе
    Все поля текущего config.json учтены в таблице. Полный отказ от config.json сейчас не фиксируется — решение зависит от того, что именно сервер будет выдавать в рантайме.

    Валидация на сервере

    При commit снапшота платформа ( api.reeel.dev ) валидирует только манифестную часть project.json и игнорирует остальное ( theme , locales , designWidth , version ; top-level entry служит лишь дефолтом). Проверяется:

    • configurations — непустой массив;
    • у каждой конфигурации есть уникальное name и target ;
    • entry (конфигурации или top-level) резолвится в реальный файл снапшота.

    Нарушения возвращают 422 manifest_invalid с errors[] (коды empty_configurations , missing_configuration_name , duplicate_configuration_name , missing_target , entry_unresolved , …). Неизвестный target — это warning , не отказ. Тот же контракт доступен как dry-run: POST …/snapshots:validate (всегда 200 ).

    Отдельно от манифеста: slug проекта имеет зарезервированные значения ( api , auth , identity , projects , …) — их использование даёт 422 reserved_slug при создании проекта. Полный список — в platform-docs/reeel-editor-api.md .

    Эволюция схемы

    • Сейчас схема v1 неявная : schemaVersion не пишется. Первое несовместимое изменение введёт явный schemaVersion (2+); инструменты должны уметь читать манифест и при необходимости предлагать миграцию.
    • Новые поля добавляются как необязательные с разумными значениями по умолчанию.
    • Редактор валидирует project.json перед синхронизацией ( ProjectService.validate() ), а сервер — при commit (см. выше и sync_and_build.md ).
    menu_baseline