PRD

Table of Contents

TextFoundry PRD: Архитектура Text Engine

Цель и назначение документа

Этот документ описывает ограничения, архитектурные принципы, жизненные циклы сущностей и дорожную карту Text Engine — движка для конструирования текстов и промптов из переиспользуемых смысловых блоков [1].

Документ служит:

  • фиксацией инженерных решений

  • защитой от расползания требований

  • контрактом между Core, Storage, Server и UI

  • ориентиром для дальнейшей реализации и стабилизации

PRD в TextFoundry описывает не только то, что уже реализовано, но и целевую архитектуру проекта. Для каждого крупного слоя важно различать:

  • текущее состояние реализации

  • зафиксированное архитектурное направление

Фактический срез текущего состояния проекта вынесен в doc/status.adoc.

Область применения

Text Engine предназначен для:

  • сборки текстов из логических блоков

  • переиспользования фрагментов между разными текстами

  • параметризации и вариативности формулировок

  • контролируемой нормализации стиля

  • явных AI-assisted workflow для генерации и rewrite

Text Engine не является:

  • LLM-фреймворком общего назначения

  • системой автоматического перевода "по умолчанию"

  • WYSIWYG-редактором

Ключевые ограничения (Design Constraints)

Принцип engine-first

Движок разрабатывается как самостоятельная библиотека [1], которая:

  • не зависит от UI, CLI или сетевого протокола

  • имеет единый публичный API

  • рассматривает любые интерфейсы как клиентов Core

Отсутствие "магии"

Запрещены операции, нарушающие детерминизм без явного запроса [1]:

  • неявное семантическое переписывание текста

  • автоматическое изменение языка или времени

  • попытки угадать намерения пользователя

Все обычные преобразования должны быть детерминированными, воспроизводимыми и тестируемыми [1].

Контроль над текстом важнее удобства

Если автоматизация приводит к потере смысла, размытию формулировок или скрытым изменениям текста [1] — такая автоматизация запрещена.

Явная граница между deterministic и AI слоями

В проекте допускаются AI-функции, но они должны оставаться отдельным opt-in слоем над core rendering:

  • обычный Render() не вызывает LLM автоматически

  • SemanticStyle сам по себе не изменяет текст

  • AI-операции запускаются только через явные API и workflow

Основные сущности и компоненты

Блок (Block)

Логический переиспользуемый фрагмент текста с характеристиками [1]:

  • уникальный идентификатор

  • тип (role, system, mission, safety, constraint, style, domain, meta)

  • шаблон содержания, параметры, теги

  • язык, версия и состояние жизненного цикла

  • описание и комментарий ревизии

Block не знает [1]:

  • где он используется

  • в каком порядке

  • в каком финальном тексте

Уточнение: Поле Type vs ID

Важно различать поле Type (категория блока) и поле ID (адрес блока):

Атрибут Тип данных Назначение

id

Строка (BlockId)

Глобально уникальный идентификатор. Принято использовать точечную нотацию (domain.subdomain.name), где первый сегмент часто совпадает с type, но это соглашение, а не требование

type

Перечисление (BlockType)

Таксономическая категория: role, system, mission, safety, constraint, style, domain, meta. Используется для фильтрации, валидации, AI-контекста и организации UI. Не влияет на логику рендеринга

Type — это metadata, не behavior

Поле type — это тег для организации хранилища и UI, а не класс или интерфейс, определяющий поведение:

  • НЕ влияет на Template Expansion — Renderer обрабатывает шаблоны всех типов одинаково

  • НЕ определяет структуру данных — у всех типов одинаковый базовый набор полей

  • Используется для:

    • фильтрации в CLI/API

    • валидации и доменной классификации

    • группировки и подсказок в UI

    • передачи более точного контекста в AI-assisted workflow

Соглашение об именовании ID

Хотя type и id — независимые поля, рекомендуется кодировать категорию в ID для человекочитаемости:

// Хорошо: ID содержит namespace, совпадающий с type
BlockDraftBuilder("role.analyst")
BlockDraftBuilder("constraint.latency")
BlockDraftBuilder("footer.signature") // допустимо и без префикса type

Важно: Block greeting.basic может иметь type = role, даже если в ID нет префикса role.. Type — это явное поле структуры, а не парсимая часть строки ID.

Архитектурный принцип: Type — Enum, не Class.

Использование enum вместо полиморфного класса гарантирует, что логика рендеринга остаётся единой и детерминированной, независимо от категории блока.

Композиция (Composition)

Composition состоит из упорядоченного списка Fragment, где каждый фрагмент может быть [1]:

  • BlockRef — ссылка на опубликованный Block с локальными переопределениями параметров. Указание версии обязательно для воспроизводимости [1]

  • StaticText — произвольный текст без параметров, не имеет собственного id и не версионируется отдельно от Composition

  • Separator — типизированный разделитель (newline, paragraph, hr)

StyleProfile

StyleProfile задаёт правила оформления текста и разделяется на два уровня:

StructuralStyle (структурный)

Детерминированные параметры, применяемые на этапе Template Expansion [1]:

  • block_wrapper — шаблон обёртки для каждого блока

  • preamble/postamble — статический текст до и после всей Composition

  • delimiter — разделитель между фрагментами

Текущее решение: различные output_format (json | xml | markdown | plain) как отдельный тип рендеринга не реализованы.

Рендерер возвращает итоговый текст, а форматирование под конкретный выходной формат должно достигаться через block_wrapper, preamble/postamble, delimiter, StaticText и структуру самой Composition.

Применяется автоматически при рендеринге, не изменяет семантику текста [1].

SemanticStyle (семантический)

Параметры, требующие переписывания текста:

  • tone

  • tense

  • target_language

  • person

  • дополнительные поля semantic rewrite policy

Ограничение: не применяется автоматически. Требует явного вызова Normalize(…​), NormalizeComposition(…​) или другого explicit AI workflow с подключенным normalizer/rewriter [1].

Renderer

Сервис рендеринга, отвечающий за преобразование Composition в текст [1]:

  • Template Expansion: подстановка параметров в Block через иерархию Defaults → Local Override → Runtime [1]

  • StructuralStyle Application: применение обёрток и разделителей [1]

  • Output Generation: формирование финального текста без модификации семантики [1]

Renderer не должен содержать скрытой AI-логики и по архитектурному контракту остаётся deterministic layer [1].

Расширенная структура Composition: Fragments

Для повышения гибкости сборки текстов Composition поддерживает гибридную структуру, содержащую не только ссылки на переиспользуемые блоки, но и статические текстовые вставки, не требующие создания отдельных сущностей Block [1].

Типы фрагментов

Composition состоит из упорядоченного списка Fragment, где каждый фрагмент может быть:

  • BlockRef — ссылка на опубликованный Block с локальными переопределениями параметров [1]

  • StaticText — произвольный текст без параметров

  • Separator — типизированный разделитель newline, paragraph, hr

Принципы использования StaticText

  • не имеет id и не версионируется отдельно — является частью структуры Composition [1]

  • не требует RenderContext для разрешения — текст используется as-is

  • не попадает в глобальное хранилище Block — не засоряет репозиторий одноразовыми строками [1]

  • редактируется и версионируется вместе с Composition

API работы с фрагментами
class Composition {
public:
    Fragment& AddBlockRef(const BlockId& blockId, Version version, Params localParams = {});
    Fragment& AddBlockRefLatest(const BlockId& blockId, Params localParams = {});
    Fragment& AddStaticText(std::string text);
    Fragment& AddSeparator(SeparatorType type);
    void InsertFragment(size_t index, Fragment fragment);
    void RemoveFragment(size_t index);
    void ClearFragments();
};

Мотивация

Использование StaticText позволяет избежать создания избыточных сущностей Block для [1]:

  • markdown-разметки и заголовков

  • одноразовых переходных фраз

  • синтаксических конструкций JSON/XML/markup

  • языкоспецифичных шаблонов, не требующих параметризации

Это соответствует принципу разделения ответственности: Block отвечает за переиспользуемую параметризованную логику, а Composition — за структуру и оформление конкретного текста [1].

Жизненный цикл сущностей

Состояния

  • Draft — редактируемая версия [1]

  • Published — зафиксированная versioned версия [1]

  • Deprecated — устаревшая, но доступная для чтения [1]

Правила переходов

  • Draft можно изменять без ограничений [1]

  • Published версии считаются immutable для пользовательского workflow [1]

  • Любое изменение Published сущности создаёт новую версию [1]

  • Deprecated версии не удаляются автоматически и не должны использоваться по умолчанию [1]

В текущей реализации жизненный цикл уже применяется и к Block, и к Composition. Кроме того, обе сущности поддерживают revision_comment, что является частью принятого version-aware editing workflow.

Этапы обработки текста

Composition
↓
Fragment traversal
↓
Template Expansion (Renderer)
↓
StructuralStyle
↓
Raw / Rendered Text
↓
(optional, explicit) Normalization / Rewrite
↓
Final Text

Разворачивание шаблонов (Template Expansion)

Детерминированная стадия [1], выполняемая Renderer:

  • подстановка параметров

  • сборка фрагментов

  • применение structural formatting

Normalization (опциональный, явный вызов)

Отдельный этап, который [1]:

  • вызывается явно через Normalize(text, semantic_style) или NormalizeComposition(…​)

  • может быть реализован через LLM

  • может быть полностью отключён

Запрещено: неявный вызов Normalization внутри Render() при наличии SemanticStyle. Это нарушает детерминизм и контроль над текстом [1].

auto rendered = engine.Render(comp_id, ctx);         // deterministic
auto normalized = engine.Normalize(text, style);     // explicit AI step

Rewrite и generation workflows

Дополнительно допускаются explicit AI workflows:

  • генерация структурированных block draft из prompt-а

  • нормализация composition с сохранением структуры

  • block-preserving rewrite существующей composition через preview/apply

Эти сценарии не заменяют обычный render path и не должны маскироваться под него.

Поддержка языков и времени (i18n)

Стратегия многоязычности

Без LLM поддерживаются только [1]:

  • языкоспецифичные версии Block

  • параметризация терминов и значений

  • нейтральные формулировки

  • статические language-specific фрагменты в Composition

Запрещено: автоматический перевод или адаптация текста под целевой язык через SemanticStyle без явного AI вызова [1].

Ограничения без LLM

Следующие функции не поддерживаются без привлечения LLM [1]:

  • полноценный перевод текста

  • автоматическое изменение времени

  • стилистическое переписывание текста

Модель хранения (Storage Model)

Storage является инфраструктурным слоем и полностью отделён от бизнес-логики движка [1].

Принципы хранения

  • движок не зависит от конкретной БД [1]

  • работа идёт только через репозитории [1]

  • формат хранения не является частью публичного API [1]

Проекты и пространства имён

Минимальная адресация сущностей [1]:

  • project_key

  • entity_type

  • entity_id

  • version

Разделение по проектам является важным требованием для будущего Prompt Server и уже присутствует в текущей схеме хранения [1].

Режимы доступа (Read/Write)

Допускается разделение режимов [1]:

  • read-write — используется UI, CLI и редакторами

  • read-only — должен использоваться будущим Prompt Server

Важно: Prompt Server по архитектурному контракту не должен работать с Draft-данными [1].

Версионирование

Storage обязан поддерживать [1]:

  • хранение версий и доступ к конкретной версии

  • неизменяемость опубликованных сущностей

Схема хранения (ObjectBox)

Сущности

Текущая ObjectBox-схема включает:

  • ObxBlock — хранит версии блоков с их metadata, template, defaults, params и revision comment

  • ObxComposition — хранит metadata композиции, versioning и style profile

  • ObxFragment — хранит упорядоченные fragment records для composition

Ранее в PRD использовалась формулировка про fragments_json внутри CompositionEntity. Это больше не соответствует текущей реализации.

Принятое текущее решение: композиция хранится через ObxComposition и отдельные записи ObxFragment, а не через единый JSON blob.

Паттерн Repository

IBlockRepository и ICompositionRepository — чистые интерфейсы (ports), не зависящие от ObjectBox [1]. Реализация на ObjectBox остаётся adapter-слоем.

Публикация и синхронизация

Операция публикации

Publishing — явная операция [1]:

  • фиксирует текущую Draft-версию

  • создаёт Published-версию

  • делает её доступной для воспроизводимого использования

Модель синхронизации

  • UI и CLI работают с editable workspace / draft workflow [1]

  • Published storage является источником стабильных версий [1]

  • обновление промпта = публикация новой версии [1]

  • старые версии продолжают обслуживаться [1]

Текущее состояние реализации sync model

На данный момент workflow публикации и новых версий уже реализован для:

  • Block

  • Composition

  • GUI editing flow

  • compare / revision-aware workflows

Интерфейсы: CLI и Terminal UI

CLI (обязательный компонент)

CLI является reference client для Text Engine и архитектурным якорем проекта [1].

Назначение CLI [1]:

  • проверка полноты Core API

  • инженерное использование без GUI

  • автоматизация и scripted usage

  • публикация, инспекция, render и validate сценарии

CLI не предназначен для визуального редактирования или замены GUI [1].

Terminal UI (TUI)

TUI — интерактивный клиент для удобства работы с Core API [1].

Архитектурные ограничения TUI [1]:

  • использует Core API напрямую, не через CLI subprocess

  • не содержит собственной логики рендеринга

  • не имеет собственного слоя хранения

TUI не является обёрткой над CLI, а отдельным клиентом Core API.

Первоначально TUI рассматривался как будущий этап. На текущий момент в проекте уже существует terminal workflow на базе FTXUI внутри приложения tf (blocks, compositions, render, settings).

Сервер промптов (Prompt Server)

Назначение и ограничения

Prompt Server — planned сервис дистрибуции и разрешения промптов [1]:

  • не редактирует данные

  • не генерирует новые версии

  • не содержит своей доменной логики рендеринга

  • использует published entities в read-only режиме

Prompt Server остаётся частью целевой архитектуры проекта, но в текущем репозитории пока не реализован как отдельный компонент.

Протокол взаимодействия (Draft v0.1)

Запрос клиента [1]:

  • project_key

  • prompt_name

  • version (optional, default: latest stable)

  • params (optional)

  • normalize (optional, boolean, default: false)

Ответ сервера (200 OK) [1]:

  • text — финальный текст

  • meta.composition_version

  • meta.blocks_used

  • meta.style_applied

Ошибки [1]:

  • 404 — Project или Prompt не найдены

  • 422 — отсутствует обязательный параметр или запрошена нормализация при отсутствии Normalizer

  • 409 — запрошена Draft-версия

Внутренняя логика: разрешает Composition, загружает зафиксированные версии Block через Storage в read-only режиме, вызывает Renderer [1].

Обработка стилей

Prompt Server должен применять только StructuralStyle автоматически [1].

SemanticStyle должен обрабатываться только по стратегии explicit normalization:

  • сервер возвращает текст после Template Expansion + StructuralStyle

  • клиент может явно запросить нормализацию

  • при отсутствии normalizer запрос на нормализацию должен завершаться ошибкой

Это сохраняет принцип детерминизма [1].

Модель доступа Prompt Server

Prompt Server должен работать только с Published сущностями [1]:

  • запрос: GET /prompts/blog.golang?v=1.0&audience=junior&normalize=false

  • обработка: render published composition + optional explicit normalization

  • ответ: финальный текст + метаданные использованных блоков

Prompt Server не должен вызывать publish/save draft методы [1].

UI (Qt/QML)

UI является клиентом движка и не содержит бизнес-логики [1].

Назначение:

  • управление блоками и сборка Composition

  • публикация версий

  • предпросмотр результата

  • compare и rewrite workflows

Текущее состояние реализации UI

На текущий момент в GUI уже реализованы:

  • Blocks

  • Compositions

  • Render

  • Settings

  • version-aware editing

  • raw prompt compare

  • Rewrite Blocks preview/apply workflow

Дорожная карта (Roadmap)

Этап 0 — Proof of Concept

  • Block, Composition

  • Renderer (базовый Template Expansion)

  • plain text output

Статус: в основном завершён.

Этап 1 — MVP

  • параметры и версии Block

  • сериализация и publishing

  • CLI (create, publish, render, validate)

  • строгая проверка версий в BlockRef [1]

Статус: завершён и расширен поверх исходного MVP.

Этап 2 — Stabilization

  • StyleProfile

  • Normalizer API

  • diff версий и расширенный поиск

  • revision-aware editing workflow

Статус: частично реализован.

Этап 3 — AI-assisted authoring

  • block generation

  • composition normalization

  • block-preserving rewrite

  • reviewable preview/apply flows

Статус: реализуется, значительная часть уже есть в codebase и GUI.

Этап 4 — Interfaces

  • Prompt Server

  • дальнейшее развитие UI

  • дополнительные bindings / integration surfaces

Статус: planned.

Границы проекта (Non-goals)

  • скрытое использование LLM [1]

  • UI как источник бизнес-логики [1]

  • Prompt Server как редактор [1]

  • автоматическое изменение текста без явного запроса [1]

  • универсальный формат данных как публичный контракт [1]

Критерии успешности

Проект считается успешным, если [1]:

  • движок используется как единый источник сборки reusable text assets

  • повторяющиеся тексты исчезают

  • изменение структуры не требует переписывания контента

  • AI-assisted workflow не ломают deterministic базовый контур

Архитектура реализации (C++ Core)

Структура слоёв

┌──────────────────────────────────────────────┐
│ CLI / TUI / Qt GUI / Prompt Server (future) │
├──────────────────────────────────────────────┤
│ TextFoundry Core                            │
│ - Engine                                    │
│ - Renderer                                  │
│ - Block / Composition / Fragment            │
│ - versioning / validation / publish         │
├──────────────────────────────────────────────┤
│ Repository Abstraction                      │
│ - IBlockRepository                          │
│ - ICompositionRepository                    │
├──────────────────────────────────────────────┤
│ ObjectBox Storage Layer                     │
│ - ObxBlock                                  │
│ - ObxComposition                            │
│ - ObxFragment                               │
└──────────────────────────────────────────────┘

Ранее PRD использовал формулировку header-only API. Она больше не соответствует текущей реализации: Core оформлен как полноценная C++ библиотека, а AI-адаптеры выделены в отдельный модуль.

Стратегия разрешения параметров

Параметры разрешаются по иерархии (от менее приоритетных к более приоритетным) [1]:

  1. Block Defaults — параметры, определённые в Block

  2. Composition Local Overridelocal_params в BlockRef

  3. Runtime Context — параметры, переданные в RenderContext

Строгие vs опциональные параметры

  • Default: Block содержит defaults. Если параметр не переопределён, используется значение по умолчанию [1]

  • Strict: Block содержит param_schema с required semantics. Отсутствие значения вызывает ошибку [1]

Версионирование ссылок (BlockRef Strictness)

При создании Composition указание версии в BlockRef является обязательным для воспроизводимости [1]:

  • API отклоняет попытку использовать published BlockRef без версии

  • исключение: use_latest / AddBlockRefLatest(…​) только для Draft Composition

Пример использования (Library Usage)

Полный цикл: инициализация хранилища, создание Draft Block с параметрами по умолчанию, публикация, создание Composition с переопределением параметров, рендеринг с Runtime-контекстом [1].

Конкретные имена методов и сигнатуры следует синхронизировать по doc/doc.adoc, поскольку API уже эволюционировал от ранних черновых примеров.

Критические ограничения реализации

  1. Immutable Published: published сущность считается read-only, изменение создаёт новую версию [1]

  2. Composition BlockRef: обязательное указание версии version для воспроизводимости [1]

  3. Rendering Failure: отсутствие параметра возвращает ошибку вместо магической подстановки [1]

  4. No Hidden AI: LLM-операции не вызываются из обычного Render() [1]

  5. Published/Read-only split: будущий Prompt Server должен работать только с published данными [1]