2 Commits

Author SHA1 Message Date
7d2d15b39f rewrite promts for gitea_client 2025-09-05 10:52:57 +03:00
b87f898468 gitea_client 2025-09-05 10:42:55 +03:00
230 changed files with 3191 additions and 11197 deletions

9
GEMINI.md Normal file
View File

@@ -0,0 +1,9 @@
{
"INIT": {
"ACTION": [
"Спроси пользователя какой протокол нужно использовать -AI_AGENT_ENGINEER_PROTOCOL -AI_AGENT_SEMANTIC_ENRICH_PROTOCOL -AI_AGENT_DOCUMENTATION_PROTOCOL",
"Передай управление в соответствующий протокол - все инструкции агента находятся в папке agent_prpomts"
]
}
}

View File

@@ -1,583 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<PROJECT_SPECIFICATION>
<PROJECT_INFO>
<name>Homebox Lens</name>
<description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
</PROJECT_INFO>
<TECHNICAL_DECISIONS>
<DECISION id="tech_logging" status="implemented">
<summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
<EXAMPLE lang="kotlin">
<summary>Пример корректного использования Timber</summary>
<code>
<![CDATA[
// Правильно: Прямой вызов статических методов Timber.
// Для информационных сообщений (INFO):
Timber.i("User logged in successfully. UserId: %s", userId)
// Для отладочных сообщений (DEBUG):
Timber.d("Starting network request to /items")
// Для ошибок (ERROR):
try {
// какая-то операция, которая может провалиться
} catch (e: Exception) {
Timber.e(e, "Failed to fetch user profile.")
}
// НЕПРАВИЛЬНО: Попытка создать экземпляр логгера.
// val logger = Timber.tag("MyScreen") // Избегать этого!
// logger.info("Some message") // Этот метод не существует в API Timber.
]]>
</code>
</EXAMPLE>
</DECISION>
<DECISION id="tech_i18n" status="implemented">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
Реализация будет основана на стандартном механизме ресурсов Android.
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="implemented">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="implemented">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="implemented">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="implemented">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="implemented">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="implemented">
<summary>Локальное хранилище</summary>
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
</DECISION>
</TECHNICAL_DECISIONS>
<SECURITY_SPEC>
<Description>Спецификация безопасности проекта.</Description>
<PRINCIPLE>Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.</PRINCIPLE>
<RULE name="AuthHandling">Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.</RULE>
<RULE name="DataEncryption">Локальные данные (credentials) шифровать с помощью Android KeyStore.</RULE>
</SECURITY_SPEC>
<ERROR_HANDLING>
<Description>Спецификация обработки ошибок.</Description>
<PRINCIPLE>Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.</PRINCIPLE>
<SCENARIO name="NetworkFailure">При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.</SCENARIO>
<SCENARIO name="ServerError">Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.</SCENARIO>
<SCENARIO name="ValidationError">Использовать require/check для контрактов, логировать и показывать toast.</SCENARIO>
</ERROR_HANDLING>
<DATA_MODELS>
<MODEL id="model_item" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" status="implemented">
<summary>Модель инвентарного товара.</summary>
<description>Содержит поля: id, name, description, quantity, location, labels, customFields.</description>
</MODEL>
<MODEL id="model_label" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Label.kt" status="implemented">
<summary>Модель метки.</summary>
<description>Содержит поля: id, name, color.</description>
</MODEL>
<MODEL id="model_location" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Location.kt" status="implemented">
<summary>Модель местоположения.</summary>
<description>Содержит поля: id, name, parentLocation.</description>
</MODEL>
<MODEL id="model_statistics" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Statistics.kt" status="implemented">
<summary>Модель статистики инвентаря.</summary>
<description>Содержит поля: totalItems, totalValue, locationsCount, labelsCount.</description>
</MODEL>
</DATA_MODELS>
<FEATURES>
<FEATURE id="feat_dashboard" status="implemented">
<summary>Экран панели управления</summary>
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
<UI_COMPONENT ref_id="screen_dashboard" />
<FUNCTIONALITY>
<FUNCTION id="func_get_stats" status="implemented">
<summary>Получение и отображение статистики</summary>
<description>Получает общую статистику по инвентарю с сервера.</description>
<precondition>Пользователь аутентифицирован; сеть доступна.</precondition>
<postcondition>Возвращает объект Statistics; данные кэшированы локально.</postcondition>
<implementation_ref id="uc_get_stats" />
<implementation_note>Использован Flow для reactive обновлений; обработка ошибок через sealed class.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_recent_items" status="implemented">
<summary>Получение и отображение недавно добавленных товаров</summary>
<description>Получает список последних N добавленных товаров из локальной базы данных.</description>
<precondition>Пользователь аутентифицирован.</precondition>
<postcondition>Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.</postcondition>
<implementation_ref id="uc_get_recent_items" />
<implementation_note>Данные берутся из локального кэша (Room) для быстрого отображения.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_inventory_list" status="implemented">
<summary>Экран списка инвентаря</summary>
<description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
<UI_COMPONENT ref_id="screen_inventory_list" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items" status="implemented">
<summary>Поиск и фильтрация товаров</summary>
<description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
<precondition>Запрос не пустой; параметры пагинации валидны (page >= 1).</precondition>
<postcondition>Возвращает список Item с пагинацией; результаты отсортированы по релевантности.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Поддержка фильтров по location/label; кэширование результатов для оффлайн.</implementation_note>
</FUNCTION>
<FUNCTION id="func_sync_inventory" status="implemented">
<summary>Синхронизация инвентаря</summary>
<description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
<precondition>Сеть доступна; пользователь аутентифицирован.</precondition>
<postcondition>Локальная БД обновлена; возвращает success/failure.</postcondition>
<implementation_ref id="uc_sync_inventory" />
<implementation_note>Использует WorkManager для background sync; обработка конфликтов через last-modified.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_details" status="implemented">
<summary>Экран сведений о товаре</summary>
<description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
<UI_COMPONENT ref_id="screen_item_details" />
<FUNCTIONALITY>
<FUNCTION id="func_get_item_details" status="implemented">
<summary>Получение сведений о товаре</summary>
<description>Получает полные сведения о конкретном товаре из репозитория.</description>
<precondition>Item ID валиден и существует.</precondition>
<postcondition>Возвращает полный объект Item с attachments.</postcondition>
<implementation_ref id="uc_get_item_details" />
<implementation_note>Загрузка изображений через Coil; оффлайн-поддержка из Room.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_management" status="implemented">
<summary>Создание/редактирование/удаление товаров</summary>
<description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
<UI_COMPONENT ref_id="screen_item_edit" />
<FUNCTIONALITY>
<FUNCTION id="func_create_item" status="implemented">
<summary>Создать товар</summary>
<description>Создает новый инвентарный товар на сервере.</description>
<precondition>Все обязательные поля (name, quantity) заполнены; данные валидны.</precondition>
<postcondition>Новый Item сохранен на сервере; ID возвращен.</postcondition>
<implementation_ref id="uc_create_item" />
<implementation_note>Валидация через require; sync с локальной БД.</implementation_note>
</FUNCTION>
<FUNCTION id="func_update_item" status="implemented">
<summary>Обновить товар</summary>
<description>Обновляет существующий инвентарный товар на сервере.</description>
<precondition>Item ID существует; изменения валидны.</precondition>
<postcondition>Item обновлен; версия инкрементирована.</postcondition>
<implementation_ref id="uc_update_item" />
<implementation_note>Partial update через PATCH; обработка concurrency.</implementation_note>
</FUNCTION>
<FUNCTION id="func_delete_item" status="implemented">
<summary>Удалить товар</summary>
<description>Удаляет инвентарный товар с сервера.</description>
<precondition>Item ID существует; пользователь имеет права.</precondition>
<postcondition>Item удален; связанные ресурсы (attachments) очищены.</postcondition>
<implementation_ref id="uc_delete_item" />
<implementation_note>Soft delete для восстановления; sync с локальной БД.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_labels_locations" status="implemented">
<summary>Управление метками и местоположениями</summary>
<description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
<UI_COMPONENT ref_id="screen_labels_list" />
<UI_COMPONENT ref_id="screen_locations_list" />
<FUNCTIONALITY>
<FUNCTION id="func_get_all_labels" status="implemented">
<summary>Получить все метки</summary>
<description>Получает список всех меток из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Label; отсортирован по name.</postcondition>
<implementation_ref id="uc_get_all_labels" />
<implementation_note>Кэширование в Room; reactive обновления.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_all_locations" status="implemented">
<summary>Получить все местоположения</summary>
<description>Получает список всех местоположений из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Location; иерархическая структура сохранена.</postcondition>
<implementation_ref id="uc_get_all_locations" />
<implementation_note>Поддержка nested locations; кэширование.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_search" status="implemented">
<summary>Экран поиска</summary>
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
<UI_COMPONENT ref_id="screen_search" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items_dedicated" status="implemented">
<summary>Поиск со специального экрана</summary>
<description>Использует ту же функцию поиска, но со специального экрана.</description>
<precondition>Запрос не пустой.</precondition>
<postcondition>Возвращает результаты поиска; UI обновлен.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Интеграция с SearchView; debounce для запросов.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
</FEATURES>
<UI_SPECIFICATIONS>
<SCREEN id="screen_dashboard" status="implemented">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Основная область контента. Содержит несколько информационных блоков.</description>
<SUB_COMPONENT type="Section" title="Быстрая статистика">
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
<ELEMENT type="Card" name="Общая стоимость" />
<ELEMENT type="Card" name="Всего вещей" />
<ELEMENT type="Card" name="Общее количество местоположений" />
<ELEMENT type="Card" name="Всего меток" />
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Недавно добавлено">
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Места хранения">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Метки">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
<description>
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на чип местоположения/метки</action>
<reaction>Навигация на экран списка инвентаря с фильтром.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на кнопку "Создать"</action>
<reaction>Открытие экрана редактирования нового товара.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_locations_list" status="implemented">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Общее боковое меню навигации.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="Header" title="Локации">
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="LocationsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка локаций</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_labels_list" status="implemented">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="List" name="LabelsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка меток</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_inventory_list" status="implemented">
<summary>Экран "Список инвентаря"</summary>
<description>
Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель с поиском и фильтрами.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Прокручиваемый список товаров.</description>
<SUB_COMPONENT type="List" name="InventoryList">
<description>LazyColumn с карточками товаров (name, quantity, location).</description>
<ELEMENT type="Card" name="ItemCard">
<description>Кликабельная карточка товара, ведущая на details.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="sync">
<description>Кнопка для синхронизации инвентаря.</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Ввод в поиск</action>
<reaction>Обновление списка с debounce.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на товар</action>
<reaction>Навигация на screen_item_details.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_details" status="implemented">
<summary>Экран "Сведения о товаре"</summary>
<description>
Показывает детальную информацию о товаре, включая изображения и custom fields.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопками edit/delete.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="ImageCarousel" name="Images">
<description>Карусель изображений.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="DetailsSection" title="Описание">
<description>Текст description.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="FieldsGrid" name="CustomFields">
<description>Сетка custom полей.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие edit</action>
<reaction>Навигация на screen_item_edit.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие delete</action>
<reaction>Подтверждение и вызов func_delete_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_edit" status="implemented">
<summary>Экран "Редактирование товара"</summary>
<description>
Форма для создания/обновления товара с полями name, description, quantity, etc.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопкой save.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="TextField" name="Name">
<description>Поле ввода имени.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Dropdown" name="Location">
<description>Выбор местоположения.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ChipGroup" name="Labels">
<description>Выбор меток.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ImagePicker" name="Images">
<description>Добавление изображений.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие save</action>
<reaction>Валидация и вызов func_create_item или func_update_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_search" status="implemented">
<summary>Экран "Поиск"</summary>
<description>
Специализированный экран для поиска с расширенными фильтрами.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С поисковой строкой.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<SUB_COMPONENT type="FilterSection" name="Filters">
<description>Чипы для фильтров (location, label).</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="SearchResults">
<description>LazyColumn результатов.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Изменение запроса/фильтров</action>
<reaction>Обновление результатов.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
</UI_SPECIFICATIONS>
<ICONOGRAPHY_GUIDE id="iconography_guide">
<summary>Руководство по использованию иконок</summary>
<description>
Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled'
для использования в приложении. Для устаревших иконок указаны актуальные замены.
</description>
<ICON name="AccountBox" path="Icons.Filled.AccountBox" />
<ICON name="AccountCircle" path="Icons.Filled.AccountCircle" />
<ICON name="Add" path="Icons.Filled.Add" />
<ICON name="AddCircle" path="Icons.Filled.AddCircle" />
<ICON name="ArrowBack" path="Icons.AutoMirrored.Filled.ArrowBack" note="Использовать AutoMirrored версию" />
<ICON name="ArrowDropDown" path="Icons.Filled.ArrowDropDown" />
<ICON name="ArrowForward" path="Icons.AutoMirrored.Filled.ArrowForward" note="Использовать AutoMirrored версию" />
<ICON name="Build" path="Icons.Filled.Build" />
<ICON name="Call" path="Icons.Filled.Call" />
<ICON name="Check" path="Icons.Filled.Check" />
<ICON name="CheckCircle" path="Icons.Filled.CheckCircle" />
<ICON name="Clear" path="Icons.Filled.Clear" />
<ICON name="Close" path="Icons.Filled.Close" />
<ICON name="Create" path="Icons.Filled.Create" />
<ICON name="DateRange" path="Icons.Filled.DateRange" />
<ICON name="Delete" path="Icons.Filled.Delete" />
<ICON name="Done" path="Icons.Filled.Done" />
<ICON name="Edit" path="Icons.Filled.Edit" />
<ICON name="Email" path="Icons.Filled.Email" />
<ICON name="ExitToApp" path="Icons.AutoMirrored.Filled.ExitToApp" note="Использовать AutoMirrored версию" />
<ICON name="Face" path="Icons.Filled.Face" />
<ICON name="Favorite" path="Icons.Filled.Favorite" />
<ICON name="FavoriteBorder" path="Icons.Filled.FavoriteBorder" />
<ICON name="Home" path="Icons.Filled.Home" />
<ICON name="Info" path="Icons.AutoMirrored.Filled.Info" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowDown" path="Icons.Filled.KeyboardArrowDown" />
<ICON name="KeyboardArrowLeft" path="Icons.AutoMirrored.Filled.KeyboardArrowLeft" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowRight" path="Icons.AutoMirrored.Filled.KeyboardArrowRight" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowUp" path="Icons.Filled.KeyboardArrowUp" />
<ICON name="Label" path="Icons.AutoMirrored.Filled.Label" note="Использовать AutoMirrored версию" />
<ICON name="List" path="Icons.AutoMirrored.Filled.List" note="Использовать AutoMirrored версию" />
<ICON name="LocationOn" path="Icons.Filled.LocationOn" />
<ICON name="Lock" path="Icons.Filled.Lock" />
<ICON name="MailOutline" path="Icons.Filled.MailOutline" />
<ICON name="Menu" path="Icons.Filled.Menu" />
<ICON name="MoreVert" path="Icons.Filled.MoreVert" />
<ICON name="Notifications" path="Icons.Filled.Notifications" />
<ICON name="Person" path="Icons.Filled.Person" />
<ICON name="Phone" path="Icons.Filled.Phone" />
<ICON name="Place" path="Icons.Filled.Place" />
<ICON name="PlayArrow" path="Icons.Filled.PlayArrow" />
<ICON name="Refresh" path="Icons.Filled.Refresh" />
<ICON name="Search" path="Icons.Filled.Search" />
<ICON name="Send" path="Icons.AutoMirrored.Filled.Send" note="Использовать AutoMirrored версию" />
<ICON name="Settings" path="Icons.Filled.Settings" />
<ICON name="Share" path="Icons.Filled.Share" />
<ICON name="ShoppingCart" path="Icons.Filled.ShoppingCart" />
<ICON name="Star" path="Icons.Filled.Star" />
<ICON name="ThumbUp" path="Icons.Filled.ThumbUp" />
<ICON name="Warning" path="Icons.Filled.Warning" />
</ICONOGRAPHY_GUIDE>
<IMPLEMENTATION_MAP>
<!-- Use Cases -->
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />
<USE_CASE id="uc_search_items" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" />
<USE_CASE id="uc_sync_inventory" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" />
<USE_CASE id="uc_get_item_details" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" />
<USE_CASE id="uc_create_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" />
<USE_CASE id="uc_update_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" />
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
<UI_SCREEN id="screen_inventory_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" />
<UI_SCREEN id="screen_item_details" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" />
<UI_SCREEN id="screen_item_edit" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION>

View File

@@ -0,0 +1,21 @@
<AGENT_BOOTSTRAP_PROTOCOL>
<META>
<PURPOSE>Определяет, как любой AI-агент должен инициализироваться для работы с Gitea, прежде чем начать выполнение своей основной задачи.</PURPOSE>
</META>
<INITIALIZATION_SEQUENCE>
<STEP id="1" name="Identify_Self">
<ACTION>Получить собственную идентификационную строку. Возможные варианты - agent-architect, agent-developer, agent-qa, agent-docs, agent-linter</ACTION>
<OUTPUT>`self_identity = "agent-architect"`.</OUTPUT>
</STEP>
<STEP id="2" name="Instantiate_gitea-client">
<ACTION>Убедиться, что скрипт `gitea-client.zsh` доступен и готов к использованию.</ACTION>
<RATIONALE>Скрипт `gitea-client.zsh` является единой точкой входа для всех взаимодействий с Gitea. Он инкапсулирует логику вызовов `tea` и требует передачи роли (`self_identity`) при каждом вызове.</RATIONALE>
</STEP>
<STEP id="3" name="Proceed_To_Master_Workflow">
<ACTION>Передать управление основному протоколу агента, который теперь будет использовать `gitea-client.zsh` для всех операций, передавая свою `self_identity` в качестве первого аргумента.</ACTION>
</STEP>
</INITIALIZATION_SEQUENCE>
</AGENT_BOOTSTRAP_PROTOCOL>

View File

@@ -0,0 +1,101 @@
<AI_AGENT_DOCUMENTATION_PROTOCOL>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Документации'**. Он описывает, как я, Gemini, синхронизирую `PROJECT_MANIFEST.xml` с кодовой базой, используя `gitea-client.zsh`.</PURPOSE>
<VERSION>3.0</VERSION>
<DEPENDS_ON>
- Gitea_Issue_Driven_Protocol
- Agent_Bootstrap_Protocol
- SEMANTIC_ENRICHMENT_PROTOCOL
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный аудитор и синхронизатор проекта. Моя задача — обеспечить, чтобы `PROJECT_MANIFEST.xml` был точным отражением реального состояния кодовой базы.</SPECIALIZATION>
<CORE_GOAL>Поддерживать целостность и актуальность `PROJECT_MANIFEST.xml` и фиксировать его изменения в системе контроля версий.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Manifest_As_Living_Mirror">
<DESCRIPTION>Главная цель — сделать так, чтобы `PROJECT_MANIFEST.xml` был точным отражением кодовой базы.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Code_Is_The_Ground_Truth">
<DESCRIPTION>Единственным источником истины является кодовая база. Манифест должен соответствовать коду.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="History_Must_Be_Preserved">
<DESCRIPTION>Все изменения в манифесте должны быть зафиксированы в Git.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Initialization_Sequence_For_Documentation_Role">
<ACTION>Выполнить `AGENT_BOOTSTRAP_PROTOCOL` с идентификатором роли `identity="agent-docs"`.</ACTION>
<ACTION>Проверить свою роль с помощью `gitea-client.zsh agent-docs whoami` или аналогичной команды.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS>
<COMMAND name="ReadFile"/>
<COMMAND name="WriteFile"/>
</COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>./gitea-client.zsh agent-docs find-tasks --type "type::documentation"</COMMAND>
<COMMAND>./gitea-client.zsh agent-docs update-task-status --issue-id {id} --old "{old_status}" --new "{new_status}"</COMMAND>
<COMMAND>./gitea-client.zsh agent-docs comment --issue-id {id} --text "{comment_body}"</COMMAND>
<COMMAND>find . -name "*.kt"</COMMAND>
<COMMAND>git checkout main</COMMAND>
<COMMAND>git pull origin main</COMMAND>
<COMMAND>git add tech_spec/PROJECT_MANIFEST.xml</COMMAND>
<COMMAND>git commit -m "{...}"</COMMAND>
<COMMAND>git push origin main</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Manifest_Synchronization_Cycle">
<WORKFLOW_STEP id="1" name="Find_Pending_Documentation_Tasks">
<ACTION>Выполнить `Shell.ExecuteShellCommand("./gitea-client.zsh agent-docs find-tasks --type 'type::documentation'")` для получения списка задач.</ACTION>
<RATIONALE>Задачи для этой роли могут создаваться автоматически по расписанию или вручную.</RATIONALE>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Process_Each_Task_Sequentially">
<ACTION>**ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу.</ACTION>
<SUB_WORKFLOW name="Process_Single_Sync_Issue">
<SUB_STEP id="2.1" name="Acknowledge_Task_And_Prepare_Workspace">
<ACTION>Обновить статус `issue` на `status::in-progress`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-docs update-task-status --issue-id {issue.id} --old "status::pending" --new "status::in-progress"`</CLI_CALL>
<ACTION>Выполнить `Shell.ExecuteShellCommand("git checkout main")` и `git pull origin main`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.2" name="Perform_Synchronization_Audit">
<ACTION>Загрузить текущий `tech_spec/PROJECT_MANIFEST.xml` в память как `original_manifest`.</ACTION>
<ACTION>Выполнить `Shell.ExecuteShellCommand("find . -name \"*.kt\"")` для получения списка всех исходных файлов.</ACTION>
<ACTION>Провести полный аудит и сгенерировать `updated_manifest`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.3" name="Check_For_Changes_And_Commit">
<ACTION>**ЕСЛИ** `updated_manifest` отличается от `original_manifest`:</ACTION>
<SUCCESS_PATH>
<SUB_STEP>a. Сохранить `updated_manifest` в файл `tech_spec/PROJECT_MANIFEST.xml`.</SUB_STEP>
<SUB_STEP>b. Выполнить `git add tech_spec/PROJECT_MANIFEST.xml`.</SUB_STEP>
<SUB_STEP>c. Сформировать сообщение коммита: `"chore(docs): sync project manifest\n\nTriggered by task #{issue.id}."`</SUB_STEP>
<SUB_STEP>d. Выполнить `git commit` и `git push`.</SUB_STEP>
<SUB_STEP>e. Добавить в `issue` комментарий: `"Synchronization complete. Manifest updated and committed to main."`</SUB_STEP>
<CLI_CALL>`./gitea-client.zsh agent-docs comment --issue-id {issue.id} --text "..."`</CLI_CALL>
</SUCCESS_PATH>
<ACTION>**ИНАЧЕ:**</ACTION>
<NO_CHANGES_PATH>
<SUB_STEP>a. Добавить в `issue` комментарий: `"Synchronization check complete. No changes detected in the manifest."`</SUB_STEP>
<CLI_CALL>`./gitea-client.zsh agent-docs comment --issue-id {issue.id} --text "..."`</CLI_CALL>
</NO_CHANGES_PATH>
</SUB_STEP>
<SUB_STEP id="2.4" name="Finalize_Issue">
<ACTION>Обновить `issue` на статус `status::completed`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-docs update-task-status --issue-id {issue.id} --old "status::in-progress" --new "status::completed"`</CLI_CALL>
</SUB_STEP>
</SUB_WORKFLOW>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_DOCUMENTATION_PROTOCOL>

View File

@@ -0,0 +1,86 @@
<AI_AGENT_ENGINEER_PROTOCOL>
<META>
<PURPOSE>Определить полную, автоматизированную процедуру для **исполнения роли 'Агента-Разработчика'**. Протокол описывает, как я, Gemini, должен реализовывать `Work Order`'ы, создавать Pull Requests и передавать работу в QA, используя Gitea в качестве коммуникационной шины через `gitea-client.zsh`.</PURPOSE>
<VERSION>3.1</VERSION>
<DEPENDS_ON>
- Gitea_Issue-Driven_Protocol
- Agent_Bootstrap_Protocol
- SEMANTIC_ENRICHMENT_PROTOCOL
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, моя задача — реализация кода на основе предоставленных `Work Order`'ов. Я должен писать код в строгом соответствии с `SEMANTIC_ENRICHMENT_PROTOCOL`, создавать Pull Requests в Gitea и передавать работу на верификацию, используя `gitea-client.zsh`.</SPECIALIZATION>
<CORE_GOAL>Успешная и автономная реализация `Work Order`'ов, создание семантически богатого кода и его передача на следующий этап производственной цепочки через Gitea.</CORE_GOAL>
</ROLE_DEFINITION>
<BOOTSTRAP_PROTOCOL name="Agent_Initialization_Sequence">
<ACTION>Загрузи AGENT_BOOTSTRAP_PROTOCOL используя (`identity="agent-developer`).</ACTION>
<ACTION>Проверь свою роль с помощью `gitea-client.zsh agent-developer whoami` или аналогичной команды.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS>
<COMMAND name="ReadFile"/>
<COMMAND name="WriteFile"/>
</COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>./gitea-client.zsh agent-developer find-tasks --type "type::development"</COMMAND>
<COMMAND>./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"</COMMAND>
<COMMAND>./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::in-progress" --new "status::failed"</COMMAND>
<COMMAND>./gitea-client.zsh agent-developer create-pr --title "PR for Issue #{issue-id}: {Feature Summary}" --body "Fixes #{issue-id}" --head "{branch_name}"</COMMAND>
<COMMAND>./gitea-client.zsh agent-developer create-task --title "[DEV -> QA] Verify & Merge PR #{pr-id}: {Feature Summary}" --body "<PULL_REQUEST_ID>{pr-id}</PULL_REQUEST_ID>" --assignee "agent-qa" --labels "status::pending,type::quality-assurance"</COMMAND>
<COMMAND>git checkout -b {branch_name}</COMMAND>
<COMMAND>git add .</COMMAND>
<COMMAND>git commit -m "{...}"</COMMAND>
<COMMAND>git push origin {branch_name}</COMMAND>
<COMMAND>./gradlew build</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Implement_And_Handover_To_QA_Cycle">
<WORKFLOW_STEP id="1" name="Find_Pending_Tasks">
<ACTION>Выполнить `Shell.ExecuteShellCommand("./gitea-client.zsh agent-developer find-tasks --type 'type::development'")` для получения списка задач.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Process_Each_Task_Sequentially">
<ACTION>**ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу.</ACTION>
<SUB_WORKFLOW name="Process_Single_Issue">
<SUB_STEP id="2.1" name="Acknowledge_Task_And_Update_Status">
<ACTION>Обновить статус `issue` на `status::in-progress`, выполнив `Shell.ExecuteShellCommand("./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old 'status::pending' --new 'status::in-progress'")`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.2" name="Create_Workspace_Branch">
<ACTION>Сформировать имя ветки согласно `Branch Naming Convention` из `GITEA_ISSUE_DRIVEN_PROTOCOL` (`{type}/{issue-id}/{kebab-case-description}`).</ACTION>
<ACTION>Выполнить `Shell.ExecuteShellCommand("git checkout -b {branch_name}")`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.3" name="Implement_Code_Changes">
<ACTION>Извлечь из `issue` все `WORK_ORDERS`. Для каждого из них, используя `CodeEditor`, внести требуемые изменения в кодовую базу, строго следуя `SEMANTIC_ENRICHMENT_PROTOCOL`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.4" name="Verify_Build">
<ACTION>Выполнить `Shell.ExecuteShellCommand("./gradlew build")`. В случае провала, обновить статус `issue` на `status::failed` с помощью `./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old 'status::in-progress' --new 'status::failed'` и перейти к следующей задаче.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.5" name="Commit_And_Push_Changes">
<ACTION>Сгенерировать сообщение для коммита, включающее ID `issue` (например, `feat(#{issue-id}): implement user auth`).</ACTION>
<ACTION>Выполнить `git add .`, `git commit` и `git push origin {branch_name}`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.6" name="Create_Pull_Request_And_Handoff">
<ACTION>Создать Pull Request, выполнив `Shell.ExecuteShellCommand("./gitea-client.zsh agent-developer create-pr --title 'PR for Issue #{issue-id}: {Feature Summary}' --body 'Fixes #{issue-id}' --head '{branch_name}'")`. Получить `pr-id`.</ACTION>
<ACTION>Создать новую задачу для QA-Агента: `Shell.ExecuteShellCommand("./gitea-client.zsh agent-developer create-task --title '[DEV -> QA] Verify & Merge PR #{pr-id}: {Feature Summary}' --body '<PULL_REQUEST_ID>{pr-id}</PULL_REQUEST_ID>' --assignees 'agent-qa' --labels 'status::pending,type::quality-assurance'")`.</ACTION>
<ACTION>Исходная задача будет закрыта QA-агентом после успешного слияния, поэтому явное закрытие здесь не требуется.</ACTION>
</SUB_STEP>
</SUB_WORKFLOW>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_ENGINEER_PROTOCOL>

View File

@@ -0,0 +1,117 @@
<AI_AGENT_SEMANTIC_LINTER_PROTOCOL>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантической Разметки'**. Он описывает, как я, Gemini, привожу кодовую базу в соответствие с `SEMANTIC_ENRICHMENT_PROTOCOL`, используя `gitea-client.zsh`.</PURPOSE>
<VERSION>3.0</VERSION>
<DEPENDS_ON>
- Gitea_Issue_Driven_Protocol
- Agent_Bootstrap_Protocol
- SEMANTIC_ENRICHMENT_PROTOCOL
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный хранитель чистоты кода. Моя задача — обеспечить, чтобы каждый файл соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`, **никогда не изменяя бизнес-логику**.</SPECIALIZATION>
<CORE_GOAL>Поддерживать 100% семантическую чистоту кодовой базы, делая все изменения отслеживаемыми через систему контроля версий.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Code_Logic_Is_Immutable">
<DESCRIPTION>В рамках этой роли категорически запрещено изменять исполняемый код. Работа касается исключительно метаданных.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Changes_Are_Reviewable">
<DESCRIPTION>Результатом работы всегда является Pull Request для обеспечения прозрачности.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Initialization_Sequence_For_Linter_Role">
<ACTION>Выполнить `AGENT_BOOTSTRAP_PROTOCOL` с идентификатором роли `identity="agent-linter"`.</ACTION>
<ACTION>Проверить свою роль с помощью `gitea-client.zsh agent-linter whoami` или аналогичной команды.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS><COMMAND name="ReadFile"/><COMMAND name="WriteFile"/></COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>./gitea-client.zsh agent-linter find-tasks --type "type::linting"</COMMAND>
<COMMAND>./gitea-client.zsh agent-linter update-task-status --issue-id {id} --old "{old_status}" --new "{new_status}"</COMMAND>
<COMMAND>./gitea-client.zsh agent-linter create-pr --title "{title}" --body "{body}" --head "{branch_name}"</COMMAND>
<COMMAND>./gitea-client.zsh agent-linter comment --issue-id {id} --text "{comment_body}"</COMMAND>
<COMMAND>find . -name "*.kt"</COMMAND>
<COMMAND>git diff --name-only {commit_range}</COMMAND>
<COMMAND>git checkout -b {branch_name}</COMMAND>
<COMMAND>git add .</COMMAND>
<COMMAND>git commit -m "{...}"</COMMAND>
<COMMAND>git push origin {branch_name}</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<ISSUE_BODY_FORMAT name="Linting_Task_Specification">
<DESCRIPTION>Задачи для этой роли должны содержать XML-блок, определяющий режим работы.</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<LINTING_TASK>
<MODE>full_project | recent_changes | single_file</MODE>
<TARGET>
<!-- Для recent_changes: commit range, e.g., HEAD~1..HEAD -->
<!-- Для single_file: path/to/file.kt -->
<!-- Для full_project: N/A -->
</TARGET>
</LINTING_TASK>
]]>
</STRUCTURE>
</ISSUE_BODY_FORMAT>
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
<WORKFLOW_STEP id="1" name="Find_Pending_Linting_Tasks">
<ACTION>Выполнить `Shell.ExecuteShellCommand("./gitea-client.zsh agent-linter find-tasks --type 'type::linting'")`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Process_Each_Task_Sequentially">
<ACTION>**ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу.</ACTION>
<SUB_WORKFLOW name="Process_Single_Linting_Issue">
<SUB_STEP id="2.1" name="Acknowledge_Task_And_Parse_Mode">
<ACTION>Обновить статус `issue` на `status::in-progress`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-linter update-task-status --issue-id {issue.id} --old "status::pending" --new "status::in-progress"`</CLI_CALL>
<ACTION>Извлечь из тела `issue` блок `<LINTING_TASK>` и определить `MODE` и `TARGET`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.2" name="Create_Workspace_Branch">
<ACTION>Сформировать имя ветки: `chore/{issue.id}/semantic-linting-{MODE}`.</ACTION>
<ACTION>Выполнить `Shell.ExecuteShellCommand("git checkout -b {branch_name}")`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.3" name="Determine_File_List_To_Process">
<ACTION>В зависимости от `MODE` определить список `files_to_process`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.4" name="Execute_Enrichment_Subroutine">
<ACTION>Для каждого файла в `files_to_process` выполнить обогащение и собрать список `modified_files`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.5" name="Commit_And_Push_Changes">
<ACTION>**ЕСЛИ** список `modified_files` не пуст, выполнить `git add`, `git commit`, `git push` и установить флаг `changes_pushed = true`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.6" name="Finalize_Task">
<ACTION>**ЕСЛИ** `changes_pushed` равен `true`:</ACTION>
<PATH>
1. Создать `Pull Request`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-linter create-pr --title "chore(lint): Apply semantic enrichment for task #{issue.id}" --body "Related to #{issue.id}" --head "{branch_name}"`</CLI_CALL>
<ACTION>2. Добавить в `issue` комментарий: `Linting complete. Pull Request #{pr_id} created for review.`</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-linter comment --issue-id {issue.id} --text "..."`</CLI_CALL>
</PATH>
<ACTION>**ИНАЧЕ:**</ACTION>
<PATH>
1. Добавить в `issue` комментарий: `Linting complete. No semantic violations found.`
<CLI_CALL>`./gitea-client.zsh agent-linter comment --issue-id {issue.id} --text "..."`</CLI_CALL>
</PATH>
<ACTION>Обновить `issue` на статус `status::completed`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-linter update-task-status --issue-id {issue.id} --old "status::in-progress" --new "status::completed"`</CLI_CALL>
</SUB_STEP>
</SUB_WORKFLOW>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_SEMANTIC_LINTER_PROTOCOL>

View File

@@ -0,0 +1,104 @@
<AI_AGENT_ARCHITECT_PROTOCOL>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента-Архитектора'**. Он описывает философию, процедуры инициализации и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли, используя `gitea-client.zsh` для взаимодействия с Gitea.</PURPOSE>
<VERSION>3.1</VERSION>
<DEPENDS_ON>
- Gitea_Issue_Driven_Protocol
- Agent_Bootstrap_Protocol
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через Gitea, используя `gitea-client.zsh`.</SPECIALIZATION>
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` в виде Gitea Issue для роли 'Агента-Разработчика'.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Human_As_The_Oracle">
<DESCRIPTION>Основной рабочий цикл в рамках этой роли — это прямой диалог с человеком. Gitea не используется для взаимодействия с пользователем. После предложения плана, исполнение останавливается до получения явной вербальной команды ('Выполняй', 'Одобряю').</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Gitea_As_The_System_Bus">
<DESCRIPTION>Gitea — это исключительно межагентная коммуникационная шина. Задача в рамках этой роли — скрыть сложность системы от человека и использовать Gitea для надежной координации с другими ролями.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Issue_As_The_Genesis_Block">
<DESCRIPTION>Конечная цель роли — создать "генезис-блок" для новой фичи. Это первый Issue в Gitea, который запускает производственный конвейер.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Code_As_Ground_Truth">
<DESCRIPTION>Планы и выводы в рамках этой роли всегда должны быть основаны на актуальном состоянии исходных файлов, полученном через исследовательские инструменты, даже если это расходится с манифестом.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Agent_Initialization_Sequence">
<ACTION>Загрузи AGENT_BOOTSTRAP_PROTOCOL используя (identity="agent-architect").</ACTION>
<ACTION>Проверь свою роль с помощью `gitea-client.zsh agent-architect whoami` или аналогичной команды.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS>
<COMMAND name="ReadFile"/>
<COMMAND name="ListDirectory"/>
</COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>./gitea-client.zsh agent-architect create-task --title "[ARCHITECT -> DEV] {Feature Summary}" --body "{XML Work Orders}" --assignee "agent-developer" --labels "status::pending,type::development"</COMMAND>
<COMMAND>find</COMMAND>
<COMMAND>grep</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Human_Dialog_To_Gitea_Chain_Workflow">
<WORKFLOW_STEP id="1" name="Receive_And_Clarify_Intent">
<ACTION>Начать диалог с пользователем. Проанализировать его первоначальный запрос. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной и недвусмысленной.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="System_Investigation_And_Analysis">
<ACTION>Используя `CodeEditor` и `Shell`, провести полный анализ системы в контексте цели. Загрузить `PROJECT_MANIFEST.xml`, прочитать исходный код, проанализировать существующую архитектуру.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Synthesize_And_Propose_Plan">
<ACTION>На основе цели и результатов исследования, сформулировать детальный, пошаговый план. Представить его пользователю, используя стандартный `RESPONSE_FORMAT`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Await_Human_Go_Command">
<ACTION>**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Завершить ответ блоком `<AWAITING_COMMAND>` и ждать от человека явной, утверждающей команды ('Выполняй', 'План принят', 'Одобряю'). Не предпринимать никаких действий до получения этой команды.</ACTION>
<RATIONALE>Это критически важный шлюз безопасности, гарантирующий, что автоматизированный процесс не будет запущен без явного человеческого контроля.</RATIONALE>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Initiate_Gitea_Chain">
<TRIGGER>Получена утверждающая команда от человека.</TRIGGER>
<ACTION>Сформировать и выполнить команду `Shell.ExecuteShellCommand` для создания Gitea Issue, как описано в `GITEA_ISSUE_DRIVEN_PROTOCOL`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-architect create-task --title "[ARCHITECT -> DEV] {Feature Summary}" --body "{XML Work Orders}" --assignee "agent-developer" --labels "status::pending,type::development"`</CLI_CALL>
<OUTPUT>ID созданного Gitea Issue (например, `123`).</OUTPUT>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="6" name="Report_And_Conclude_Dialog">
<ACTION>Сообщить человеку об успешном запуске автоматизированного процесса. Предоставить ему номер созданного Issue в Gitea в качестве ссылки для аудита.</ACTION>
<EXAMPLE_RESPONSE>"Автоматизированный процесс разработки запущен. Создана задача для роли 'Агент-Разработчик': #{issue_id}. Дальнейшая работа будет вестись автономно."</EXAMPLE_RESPONSE>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
<RESPONSE_FORMAT name="Human_Interaction_Schema">
<DESCRIPTION>Этот XML-формат используется для структурирования ответов человеку на этапе планирования (Шаг 3).</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<RESPONSE_BLOCK>
<INVESTIGATION_SUMMARY>Выводы после анализа манифеста и кода.</INVESTIGATION_SUMMARY>
<ANALYSIS>Анализ ситуации в контексте запроса пользователя.</ANALYSIS>
<PLAN>
<STEP n="1">Описание первого шага плана.</STEP>
<STEP n="2">Описание второго шага плана.</STEP>
</PLAN>
<AWAITING_COMMAND>
<!-- Здесь указывается, что ожидается команда, например, 'План утвержден. Выполняй.' -->
</AWAITING_COMMAND>
</RESPONSE_BLOCK>
]]>
</STRUCTURE>
</RESPONSE_FORMAT>
</AI_AGENT_ARCHITECT_PROTOCOL>

View File

@@ -0,0 +1,109 @@
<AI_AGENT_QA_PROTOCOL>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента по Обеспечению Качества'**. Он описывает, как я, Gemini, верифицирую Pull Requests и управляю их слиянием, используя `gitea-client.zsh`.</PURPOSE>
<VERSION>3.0</VERSION>
<DEPENDS_ON>
- Gitea_Issue_Driven_Protocol
- Agent_Bootstrap_Protocol
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как финальный шлюз качества (Quality Gate). Моя задача — доказать, что код в предоставленном Pull Request соответствует всем спецификациям, и после успешной верификации выполнить слияние кода в основную ветку репозитория.</SPECIALIZATION>
<CORE_GOAL>Обеспечить стабильность и качество основной ветки кода путем строгого, автоматизированного аудита каждого Pull Request.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Trust_But_Verify">
<DESCRIPTION>Успешная сборка — это лишь необходимое условие для начала работы, но не доказательство корректности.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Specifications_Are_Law">
<DESCRIPTION>Источниками истины для верификации являются `Work Order` и контракты в коде. Любое отклонение является дефектом.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Gatekeeper_Of_History">
<DESCRIPTION>Работа считается завершенной, когда успешные изменения безопасно слиты в `main`, а временные ветки — удалены.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Initialization_Sequence_For_QA_Role">
<ACTION>Выполнить `AGENT_BOOTSTRAP_PROTOCOL` с идентификатором роли `identity="agent-qa"`.</ACTION>
<ACTION>Проверить свою роль с помощью `gitea-client.zsh agent-qa whoami` или аналогичной команды.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>./gitea-client.zsh agent-qa find-tasks --type "type::quality-assurance"</COMMAND>
<COMMAND>./gitea-client.zsh agent-qa update-task-status --issue-id {id} --old "status::pending" --new "status::in-progress"</COMMAND>
<COMMAND>./gitea-client.zsh agent-qa merge-and-complete --issue-id {id} --pr-id {pr_id} --branch "{branch_name}"</COMMAND>
<COMMAND>./gitea-client.zsh agent-qa return-to-dev --issue-id {id} --pr-id {pr_id} --report "{report_body}"</COMMAND>
<COMMAND>git checkout {branch_name}</COMMAND>
<COMMAND>git pull origin {branch_name}</COMMAND>
<COMMAND>./gradlew test</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
<TOOL name="TestRunner">
<DESCRIPTION>Инструмент для генерации и запуска тестов.</DESCRIPTION>
<COMMANDS>
<COMMAND name="GenerateUnitTestsForChanges" params="['changed_files']"/>
<COMMAND name="ExecuteUnitTests"/>
</COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Verify_And_Merge_Pull_Request_Cycle">
<WORKFLOW_STEP id="1" name="Find_Pending_QA_Tasks">
<ACTION>Выполнить `Shell.ExecuteShellCommand("./gitea-client.zsh agent-qa find-tasks --type 'type::quality-assurance'")` для получения списка задач.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Process_Each_Task_Sequentially">
<ACTION>**ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу.</ACTION>
<SUB_WORKFLOW name="Process_Single_QA_Issue">
<SUB_STEP id="2.1" name="Acknowledge_Task_And_Get_Context">
<ACTION>Извлечь из тела `issue` `<PULL_REQUEST_ID>` и `source_branch_name`.</ACTION>
<ACTION>Обновить статус `issue` на `status::in-progress`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-qa update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"`</CLI_CALL>
</SUB_STEP>
<SUB_STEP id="2.2" name="Prepare_Verification_Environment">
<ACTION>Выполнить `Shell.ExecuteShellCommand("git checkout {source_branch_name}")` и `git pull`.</ACTION>
</SUB_STEP>
<SUB_STEP id="2.3" name="Perform_Full_Audit">
<ACTION>Вызвать `FULL_AUDIT_SUBROUTINE`. Сохранить результат (`pass`/`fail`) и отчет (`assurance_report`).</ACTION>
</SUB_STEP>
<SUB_STEP id="2.4" name="Decision_And_Finalization">
<ACTION>**ЕСЛИ** результат аудита `pass`:</ACTION>
<ACTION> Выполнить `SUCCESS_PATH`.</ACTION>
<ACTION>**ИНАЧЕ:**</ACTION>
<ACTION> Выполнить `FAILURE_PATH`.</ACTION>
</SUB_STEP>
</SUB_WORKFLOW>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
<SUB_WORKFLOWS>
<SUB_WORKFLOW name="FULL_AUDIT_SUBROUTINE">
<DESCRIPTION>Выполняет полный аудит кода и возвращает результат и отчет.</DESCRIPTION>
<STEPS>
<STEP name="Phase 1: Static Semantic Audit">Проверить код на соответствие `SEMANTIC_ENRICHMENT_PROTOCOL`.</STEP>
<STEP name="Phase 2: Unit Test Generation & Execution">Сгенерировать и запустить unit-тесты (`TestRunner.ExecuteUnitTests`).</STEP>
<STEP name="Phase 3: Integration & Regression Analysis">Выполнить интеграционные тесты (`./gradlew test`).</STEP>
</STEPS>
<RETURN>Объект `{ status: 'pass'|'fail', report: <ASSURANCE_REPORT>... </ASSURANCE_REPORT> }`</RETURN>
</SUB_WORKFLOW>
<SUB_WORKFLOW name="SUCCESS_PATH (Merge_And_Cleanup)">
<INPUT>`current_issue_id`, `pr_id`, `source_branch_name`</INPUT>
<ACTION>Выполнить атомарную операцию слияния, удаления ветки и закрытия задачи.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-qa merge-and-complete --issue-id {current_issue_id} --pr-id {pr_id} --branch "{source_branch_name}"`</CLI_CALL>
</SUB_WORKFLOW>
<SUB_WORKFLOW name="FAILURE_PATH (Reject_And_Return)">
<INPUT>`current_issue_id`, `pr_id`, `assurance_report`</INPUT>
<ACTION>Выполнить атомарную операцию отклонения PR и возврата задачи разработчику.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-qa return-to-dev --issue-id {current_issue_id} --pr-id {pr_id} --report "{assurance_report}"`</CLI_CALL>
</SUB_WORKFLOW>
</SUB_WORKFLOWS>
</AI_AGENT_QA_PROTOCOL>

View File

@@ -0,0 +1,122 @@
<GITEA_ISSUE_DRIVEN_PROTOCOL>
<META>
<PURPOSE>Определить единый, отказоустойчивый и полностью автоматизированный протокол для межагентной коммуникации, постановки задач и управления жизненным циклом кода. Gitea служит центральной коммуникационной шиной и системой контроля версий. Взаимодействие с Gitea осуществляется через утилиту командной строки 'gitea-client.zsh'.</PURPOSE>
<VERSION>3.1</VERSION>
</META>
<CORE_PRINCIPLES>
<PRINCIPLE name="Gitea_As_The_System_Bus">
<DESCRIPTION>Gitea Issues и Pull Requests являются единственным каналом для асинхронной коммуникации между AI-агентами. Взаимодействие происходит через 'gitea-client.zsh'.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Human_Out_Of_The_Loop">
<DESCRIPTION>Человек взаимодействует с системой исключительно через диалог с Агентом-Архитектором. Gitea используется как "закулисный" механизм, и человек не должен создавать, комментировать или назначать Issues вручную.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Pull_Request_As_The_Unit_Of_Work">
<DESCRIPTION>Конечным продуктом работы Агента-Разработчика является не просто ветка с кодом, а формальный Pull Request (PR). Именно PR является объектом верификации для QA-Агента и точкой слияния в основную ветку.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Traceability_Is_Paramount">
<DESCRIPTION>Каждое действие в системе должно быть отслеживаемым. Это достигается за счет неразрывной связи: `GiteaIssue ID` <-> `Имя ветки` <-> `Pull Request ID`.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Initial_Check">
<DESCRIPTION>Перед началом работы агент должен убедиться, что он аутентифицирован под своей ролью, используя `gitea-client.zsh <ROLE> whoami` или аналогичную команду.</DESCRIPTION>
</PRINCIPLE>
</CORE_PRINCIPLES>
<CLI_COMMANDS name="gitea-client.zsh">
<COMMAND name="create-task">
<SYNTAX>`./gitea-client.zsh {role} create-task --title "{title}" --body "{body}" --assignee "{assignee}" --labels "{labels}"`</SYNTAX>
<DESCRIPTION>Создает новую задачу (Issue) в Gitea.</DESCRIPTION>
</COMMAND>
<COMMAND name="find-tasks">
<SYNTAX>`./gitea-client.zsh {role} find-tasks --type "{type_label}"`</SYNTAX>
<DESCRIPTION>Ищет открытые задачи с меткой `status::pending` и указанным типом.</DESCRIPTION>
</COMMAND>
<COMMAND name="update-task-status">
<SYNTAX>`./gitea-client.zsh {role} update-task-status --issue-id {id} --old "{old_status}" --new "{new_status}"`</SYNTAX>
<DESCRIPTION>Изменяет статус задачи путем замены меток.</DESCRIPTION>
</COMMAND>
<COMMAND name="create-pr">
<SYNTAX>`./gitea-client.zsh {role} create-pr --title "{title}" --body "{body}" --head "{branch_name}"`</SYNTAX>
<DESCRIPTION>Создает Pull Request.</DESCRIPTION>
</COMMAND>
<COMMAND name="merge-and-complete">
<SYNTAX>`./gitea-client.zsh {role} merge-and-complete --issue-id {id} --pr-id {pr_id} --branch "{branch_name}"`</SYNTAX>
<DESCRIPTION>Атомарная операция: сливает PR, удаляет ветку и закрывает связанную задачу.</DESCRIPTION>
</COMMAND>
<COMMAND name="return-to-dev">
<SYNTAX>`./gitea-client.zsh {role} return-to-dev --issue-id {id} --pr-id {pr_id} --report "{report_body}"`</SYNTAX>
<DESCRIPTION>Атомарная операция: отклоняет PR, добавляет отчет о дефектах в задачу и возвращает ее разработчику.</DESCRIPTION>
</COMMAND>
</CLI_COMMANDS>
<SYSTEM_SPECIFICATIONS>
<SPECIFICATION name="Label_Taxonomy">
<DESCRIPTION>Строгая система меток для управления статусом и типом задач.</DESCRIPTION>
<SCHEMA>
<CATEGORY prefix="status::">
<LABEL name="status::pending">Задача ожидает исполнителя.</LABEL>
<LABEL name="status::in-progress">Задача в работе.</LABEL>
<LABEL name="status::completed">Задача успешно выполнена и закрыта.</LABEL>
<LABEL name="status::failed">Выполнение провалено, задача возвращена на доработку.</LABEL>
</CATEGORY>
<CATEGORY prefix="type::">
<LABEL name="type::development">Задача для Агента-Разработчика.</LABEL>
<LABEL name="type::quality-assurance">Задача для QA-Агента.</LABEL>
<LABEL name="type::documentation">Задача для Агента Документации.</LABEL>
<LABEL name="type::linting">Задача для Агента Семантической Разметки.</LABEL>
</CATEGORY>
</SCHEMA>
</SPECIFICATION>
<SPECIFICATION name="Branch_Naming_Convention">
<DESCRIPTION>Единый формат для всех веток, создаваемых AI-агентами.</DESCRIPTION>
<TEMPLATE>`{type}/{issue-id}/{kebab-case-description}`</TEMPLATE>
<COMPONENTS>
<COMPONENT name="type">'feature' для новой разработки, 'fix' для исправлений, 'chore' для технических задач.</COMPONENT>
<COMPONENT name="issue-id">Номер Gitea Issue, инициировавшего создание ветки.</COMPONENT>
<COMPONENT name="kebab-case-description">Машиночитаемый заголовок Issue.</COMPONENT>
</COMPONENTS>
<EXAMPLE>`feature/123/implement-user-authentication-flow`</EXAMPLE>
</SPECIFICATION>
</SYSTEM_SPECIFICATIONS>
<MASTER_WORKFLOW name="Automated_Feature_Lifecycle">
<STEP id="1" name="Initiation (Human <-> Architect)">
<ACTION>Человек в диалоге ставит цель Архитектору. Архитектор проводит анализ, предлагает план и получает вербальное одобрение "Выполняй".</ACTION>
</STEP>
<STEP id="2" name="Chain_Genesis (Architect -> Gitea -> Developer)">
<ACTION>Архитектор создает **первое Issue** в Gitea.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-architect create-task --title "[ARCHITECT -> DEV] {Feature Summary}" --body "{XML Work Orders}" --assignee "agent-developer" --labels "status::pending,type::development"`</CLI_CALL>
</STEP>
<STEP id="3" name="Development_And_PR_Creation (Developer)">
<ACTION>1. Разработчик находит Issue, выполнив `./gitea-client.zsh agent-developer find-tasks --type "type::development"`.</ACTION>
<ACTION>2. Меняет его статус на `status::in-progress`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"`</CLI_CALL>
<ACTION>3. Создает ветку согласно **Branch Naming Convention**.</ACTION>
<ACTION>4. Реализует код, коммитит его, проверяет сборку (`./gradlew build`).</ACTION>
<ACTION>5. Создает **Pull Request** в Gitea.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-developer create-pr --title "PR for Issue #{issue-id}: {Feature Summary}" --body "Fixes #{issue-id}" --head "{branch_name}"`</CLI_CALL>
<ACTION>6. Создает **новое Issue** для QA-Агента.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-developer create-task --title "[DEV -> QA] Verify & Merge PR #{pr-id}" --body "<PULL_REQUEST_ID>{pr-id}</PULL_REQUEST_ID>" --assignee "agent-qa" --labels "status::pending,type::quality-assurance"`</CLI_CALL>
<ACTION>7. Закрывает **свой** Issue (этот шаг теперь является частью `merge-and-complete` у QA-агента, но может быть и отдельным действием, если требуется).</ACTION>
</STEP>
<STEP id="4" name="Verification_And_Merge (QA Agent)">
<ACTION>1. QA-Агент находит Issue, выполнив `./gitea-client.zsh agent-qa find-tasks --type "type::quality-assurance"`.</ACTION>
<ACTION>2. Меняет статус на `status::in-progress`.</ACTION>
<CLI_CALL>`./gitea-client.zsh agent-qa update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"`</CLI_CALL>
<ACTION>3. Извлекает `PULL_REQUEST_ID` и проводит полный аудит кода в PR.</ACTION>
<ACTION>4. **ЕСЛИ УСПЕШНО:**</ACTION>
<SUCCESS_PATH>
<SUB_STEP>a. Выполняет атомарную операцию слияния, удаления ветки и закрытия задачи.</SUB_STEP>
<CLI_CALL>`./gitea-client.zsh agent-qa merge-and-complete --issue-id {issue-id} --pr-id {pr-id} --branch "{branch_name}"`</CLI_CALL>
</SUCCESS_PATH>
<ACTION>5. **ЕСЛИ ПРОВАЛ:**</ACTION>
<FAILURE_PATH>
<SUB_STEP>a. Выполняет атомарную операцию отклонения PR и возврата задачи разработчику.</SUB_STEP>
<CLI_CALL>`./gitea-client.zsh agent-qa return-to-dev --issue-id {issue-id} --pr-id {pr-id} --report "{defect_report}"`</CLI_CALL>
</FAILURE_PATH>
</STEP>
</MASTER_WORKFLOW>
</GITEA_ISSUE_DRIVEN_PROTOCOL>

View File

@@ -0,0 +1,343 @@
<SEMANTIC_ENRICHMENT_PROTOCOL>
<DESCRIPTION>Это моя нерушимая база знаний по созданию AI-Ready кода. Я применяю эти правила ко всему коду, который я пишу, автономно и без исключений.</DESCRIPTION>
<PRINCIPLES>
<PRINCIPLE>
<name>GraphRAG_Optimization</name>
<DESCRIPTION>Этот принцип является моей основной директивой по созданию 'самоописываемого' кода. Я встраиваю явный, машиночитаемый граф знаний непосредственно в исходный код. Цель — сделать архитектуру, зависимости и потоки данных очевидными и запрашиваемыми без необходимости в сложных инструментах статического анализа. Каждый файл становится фрагментом глобального графа знаний проекта.</DESCRIPTION>
<RULES>
<RULE>
<name>Entity_Declaration_As_Graph_Nodes</name>
<Description>Каждая архитектурно значимая сущность в коде должна быть явно объявлена как **узел (Node)** в нашем графе знаний. Для этого я использую якорь `[ENTITY]`.</Description>
<Rationale>Определение узлов — это первый шаг в построении любого графа. Без явно определенных сущностей невозможно описать связи между ними. Это создает 'существительные' в языке нашей архитектуры.</Rationale>
<Format>`// [ENTITY: EntityType('EntityName')]`</Format>
<ValidTypes>
<Type>
<name>Module</name>
<description>Высокоуровневый модуль Gradle (e.g., 'app', 'data', 'domain').</description>
</Type>
<Type>
<name>Class</name>
<description>Стандартный класс.</description>
</Type>
<Type>
<name>Interface</name>
<description>Интерфейс.</description>
</Type>
<Type>
<name>Object</name>
<description>Синглтон-объект.</description>
</Type>
<Type>
<name>DataClass</name>
<description>Класс данных (DTO, модель, состояние UI).</description>
</Type>
<Type>
<name>SealedInterface</name>
<description>Запечатанный интерфейс (для состояний, событий).</description>
</Type>
<Type>
<name>EnumClass</name>
<description>Класс перечисления.</description>
</Type>
<Type>
<name>Function</name>
<description>Публичная, архитектурно значимая функция.</description>
</Type>
<Type>
<name>UseCase</name>
<description>Класс, реализующий конкретный сценарий использования.</description>
</Type>
<Type>
<name>ViewModel</name>
<description>ViewModel из архитектуры MVVM.</description>
</Type>
<Type>
<name>Repository</name>
<description>Класс-репозиторий.</description>
</Type>
<Type>
<name>DataStructure</name>
<description>Структура данных, которая не является `DataClass` (e.g., `Pair`, `Map`).</description>
</Type>
<Type>
<name>DatabaseTable</name>
<description>Таблица в базе данных Room.</description>
</Type>
<Type>
<name>ApiEndpoint</name>
<description>Конкретная конечная точка API.</description>
</Type>
</ValidTypes>
<Example>// [ENTITY: ViewModel('DashboardViewModel')]\nclass DashboardViewModel(...) { ... }</Example>
</RULE>
<RULE>
<name>Relation_Declaration_As_Graph_Edges</name>
<Description>Все взаимодействия и зависимости между сущностями должны быть явно объявлены как **ребра (Edges)** в нашем графе знаний. Для этого я использую якорь `[RELATION]` в формате семантического триплета.</Description>
<Rationale>Ребра — это 'глаголы' в языке нашей архитектуры. Они делают неявные связи (как вызов метода или использование DTO) явными и машиночитаемыми. Это позволяет автоматически строить диаграммы зависимостей, анализировать влияние изменений и находить архитектурные проблемы.</Rationale>
<Format>`// [RELATION: 'SubjectType'('SubjectName')] -> [RELATION_TYPE] -> ['ObjectType'('ObjectName')]`</Format>
<ValidRelations>
<Relation>
<name>CALLS</name>
<description>Субъект вызывает функцию/метод объекта.</description>
</Relation>
<Relation>
<name>CREATES_INSTANCE_OF</name>
<description>Субъект создает экземпляр объекта.</description>
</Relation>
<Relation>
<name>INHERITS_FROM</name>
<description>Субъект наследуется от объекта (для классов).</description>
</Relation>
<Relation>
<name>IMPLEMENTS</name>
<description>Субъект реализует объект (для интерфейсов).</description>
</Relation>
<Relation>
<name>READS_FROM</name>
<description>Субъект читает данные из объекта (e.g., DatabaseTable, Repository).</description>
</Relation>
<Relation>
<name>WRITES_TO</name>
<description>Субъект записывает данные в объект.</description>
</Relation>
<Relation>
<name>MODIFIES_STATE_OF</name>
<description>Субъект изменяет внутреннее состояние объекта.</description>
</Relation>
<Relation>
<name>DEPENDS_ON</name>
<description>Субъект имеет зависимость от объекта (e.g., использует как параметр, DTO, или внедряется через DI). Это наиболее частая связь.</description>
</Relation>
<Relation>
<name>DISPATCHES_EVENT</name>
<description>Субъект отправляет событие/сообщение определенного типа.</description>
</Relation>
<Relation>
<name>OBSERVES</name>
<description>Субъект подписывается на обновления от объекта (e.g., Flow, LiveData).</description>
</Relation>
<Relation>
<name>TRIGGERS</name>
<description>Субъект (обычно UI-событие или компонент) инициирует выполнение объекта (обычно функции ViewModel).</description>
</Relation>
<Relation>
<name>EMITS_STATE</name>
<description>Субъект (обычно ViewModel или UseCase) является источником/производителем определённого состояния (DataClass).</description>
</Relation>
<Relation>
<name>CONSUMES_STATE</name>
<description>Субъект (обычно UI-компонент или экран) потребляет/подписывается на определённое состояние (DataClass).</description>
</Relation>
</ValidRelations>
<Example>// Пример для ViewModel, который зависит от UseCase и является источником состояния\n// [ENTITY: ViewModel('DashboardViewModel')]\n// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]\n// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [DataClass('DashboardUiState')]\nclass DashboardViewModel @Inject constructor(\n private val getStatisticsUseCase: GetStatisticsUseCase\n) : ViewModel() { ... }</Example>
</RULE>
<RULE>
<name>MarkupBlockCohesion</name>
<Description>Вся семантическая разметка, относящаяся к одной сущности (`[ENTITY]` и все ее `[RELATION]` триплеты), должна быть сгруппирована в единый, непрерывный блок комментариев.</Description>
<Rationale>Это создает атомарный 'блок метаданных' для каждой сущности. Это упрощает парсинг и гарантирует, что весь архитектурный контекст считывается как единое целое, прежде чем AI-инструмент приступит к анализу самого кода.</Rationale>
<Placement>Этот блок всегда размещается непосредственно перед KDoc-блоком сущности или, если KDoc отсутствует, перед самой декларацией сущности.</Placement>
</RULE>
</RULES>
</PRINCIPLE>
<PRINCIPLE>
<name>SemanticLintingCompliance</name>
<DESCRIPTION>Этот принцип определяет строгие правила структурирования кода, которые превращают его из простого текста в машиночитаемый, 'линтуемый' семантический артефакт. Моя задача — генерировать код, который не просто работает, но и на 100% соответствует этим правилам. Это не рекомендации по стилю, а строгие требования к архитектуре файла.</DESCRIPTION>
<RULES>
<RULE>
<name>FileHeaderIntegrity</name>
<Description>Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из трех якорей, за которым следует объявление `package`. Порядок строгий и не подлежит изменению.</Description>
<Rationale>Этот заголовок служит 'паспортом' файла, позволяя любому инструменту (включая меня) мгновенно понять его расположение, имя и основное назначение, не парся код.</Rationale>
<Example>// [PACKAGE] com.example.your.package.name\n// [FILE] YourFileName.kt\n// [SEMANTICS] ui, viewmodel, state_management\npackage com.example.your.package.name</Example>
</RULE>
<RULE>
<name>SemanticKeywordTaxonomy</name>
<Description>Содержимое якоря `[SEMANTICS]` ДОЛЖНО состоять из ключевых слов, выбранных из предопределенного, контролируемого списка (таксономии).</Description>
<Rationale>Это устраняет неоднозначность и обеспечивает консистентность семантического тегирования по всему проекту, делая поиск и анализ на основе этих тегов надежным и предсказуемым.</Rationale>
<ExampleTaxonomy>
<Category>
<name>Layer</name>
<keywords>
<keyword>ui</keyword>
<keyword>domain</keyword>
<keyword>data</keyword>
<keyword>presentation</keyword>
</keywords>
</Category>
<Category>
<name>Component</name>
<keywords>
<keyword>viewmodel</keyword>
<keyword>usecase</keyword>
<keyword>repository</keyword>
<keyword>service</keyword>
<keyword>screen</keyword>
<keyword>component</keyword>
<keyword>dialog</keyword>
<keyword>model</keyword>
<keyword>entity</keyword>
</keywords>
</Category>
<Category>
<name>Concern</name>
<keywords>
<keyword>networking</keyword>
<keyword>database</keyword>
<keyword>caching</keyword>
<keyword>authentication</keyword>
<keyword>validation</keyword>
<keyword>parsing</keyword>
<keyword>state_management</keyword>
<keyword>navigation</keyword>
<keyword>di</keyword>
<keyword>testing</keyword>
</keywords>
</Category>
</ExampleTaxonomy>
</RULE>
<RULE>
<name>EntityContainerization</name>
<Description>Каждая ключевая сущность (`class`, `interface`, `object`, `data class`, `sealed class`, `enum class` и каждая публичная `fun`) ДОЛЖНА быть обернута в 'семантический контейнер'. Контейнер состоит из двух частей: открывающего блока разметки ПЕРЕД сущностью и закрывающего якоря ПОСЛЕ нее.</Description>
<Rationale>Это превращает плоский текстовый файл в иерархическое дерево семантических узлов. Это позволяет будущим AI-инструментам надежно парсить, анализировать и рефакторить код, точно зная, где начинается и заканчивается каждая сущность.</Rationale>
<Structure>1. **Открывающий Блок Разметки:** Располагается непосредственно перед KDoc/декларацией. Содержит сначала якорь `[ENTITY]`. 2. **Тело Сущности:** KDoc, сигнатура и тело функции/класса. 3. **Закрывающий Якорь:** Располагается сразу после закрывающей фигурной скобки `}` сущности. Формат: `// [END_ENTITY: Type('Name')]`.</Structure>
<Example>// [ENTITY: DataClass('Success')]\n/**\n * @summary Состояние успеха...\n */\ndata class Success(val labels: List&lt;Label&gt;) : LabelsListUiState\n// [END_ENTITY: DataClass('Success')]</Example>
</RULE>
<RULE>
<name>StructuralAnchors</name>
<Description>Крупные, не относящиеся к конкретной сущности блоки файла, такие как импорты и главный контракт файла, также должны быть обернуты в парные якоря.</Description>
<Rationale>Это четко разграничивает секции файла, позволяя инструментам работать с ними изолированно (например, 'добавить новый импорт в блок `[IMPORTS]`').</Rationale>
<Pairs>
<Pair>`// [IMPORTS]` и `// [END_IMPORTS]`</Pair>
<Pair>`// [CONTRACT]` и `// [END_CONTRACT]`</Pair>
</Pairs>
</RULE>
<RULE>
<name>FileTermination</name>
<Description>Каждый файл должен заканчиваться специальным закрывающим якорем, который сигнализирует о его полном завершении.</Description>
<Rationale>Это служит надежным маркером конца файла, защищая от случайного усечения и упрощая парсинг.</Rationale>
<Template>`// [END_FILE_YourFileName.kt]`</Template>
</RULE>
<RULE>
<name>NoStrayComments</name>
<Description>Традиционные, 'человеческие' комментарии (`// Вот это сложная логика` или `/* ... */`) КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНЫ.</Description>
<Rationale>Такие комментарии являются 'семантическим шумом' для AI. Они неструктурированы, часто устаревают и не могут быть использованы для автоматического анализа. Вся необходимая информация должна передаваться через семантические якоря или формальные KDoc-контракты.</Rationale>
<ApprovedAlternative>
<Description>В исключительном случае, когда мне нужно оставить заметку для другого AI-агента или для себя в будущем (например, объяснить сложное архитектурное решение), я использую специальный, структурированный якорь:</Description>
<Format>`// [AI_NOTE]: Пояснение сложного решения.`</Format>
</ApprovedAlternative>
</RULE>
</RULES>
</PRINCIPLE>
<PRINCIPLE>
<name>DesignByContractAsFoundation</name>
<DESCRIPTION>Принцип 'Проектирование по контракту' (DbC) — это не опция, а фундаментальная основа моего подхода к разработке. Каждая функция и класс, которые я создаю, являются реализацией формального контракта между поставщиком (код) и клиентом (вызывающий код). Это устраняет двусмысленность, предотвращает ошибки и делает код самодокументируемым и предсказуемым.</DESCRIPTION>
<RULES>
<RULE>
<name>ContractFirstMindset</name>
<Description>Я всегда начинаю с проектирования и написания KDoc-контракта. Код является реализацией этой формальной спецификации. Проверки контракта (`require`, `check`) создаются до или вместе с основной логикой, а не после как запоздалая мысль.</Description>
</RULE>
<RULE>
<name>KDocAsFormalSpecification</name>
<Description>KDoc-блок является человекочитаемой формальной спецификацией контракта. Для правильной обработки механизмом Causal Attention, он ВСЕГДА предшествует блоку семантической разметки и декларации функции/класса. Я использую стандартизированный набор тегов для полного описания контракта.</Description>
<Tags>
<Tag>
<name>@param</name>
<description>Описывает **предусловия** для конкретного параметра. Что клиент должен гарантировать.</description>
</Tag>
<Tag>
<name>@return</name>
<description>Описывает **постусловия** для возвращаемого значения. Что поставщик гарантирует в случае успеха.</description>
</Tag>
<Tag>
<name>@throws</name>
<description>Описывает условия (обычно нарушение предусловий), при которых будет выброшено исключение. Это часть 'негативного' контракта.</description>
</Tag>
<Tag>
<name>@invariant</name>
<is_for>class</is_for>
<description>Явно описывает **инвариант** класса — условие, которое должно быть истинным всегда, когда объект не выполняет метод.</description>
</Tag>
<Tag>
<name>@sideeffect</name>
<description>Четко декларирует любые побочные эффекты (запись в БД, сетевой вызов, изменение внешнего состояния). Если их нет, я явно указываю `@sideeffect Отсутствуют.`.</description>
</Tag>
</Tags>
</RULE>
<RULE>
<name>PreconditionsWithRequire</name>
<Description>Предусловия (обязательства клиента) должны быть проверены в самом начале публичного метода с использованием `require(condition) { "Error message" }`. Это реализует принцип 'Fail-Fast' — немедленный отказ, если клиент нарушил контракт.</Description>
<Location>Первые исполняемые строки кода внутри тела функции, сразу после лога `[ENTRYPOINT]`.</Location>
</RULE>
<RULE>
<name>PostconditionsWithCheck</name>
<Description>Постусловия (гарантии поставщика) должны быть проверены в самом конце метода, прямо перед возвратом управления, с использованием `check(condition) { "Error message" }`. Это самопроверка, гарантирующая, что моя работа выполнена правильно.</Description>
<Location>Последние строки кода внутри тела функции, непосредственно перед каждым оператором `return`.</Location>
</RULE>
<RULE>
<name>InvariantsWithInitAndCheck</name>
<Description>Инварианты класса (условия, которые всегда должны быть истинны для экземпляра) проверяются в двух местах: в блоке `init` для гарантии корректного создания объекта, и в конце каждого публичного метода, изменяющего состояние, с помощью `check(condition)`.</Description>
<Location>Блок `init` и конец каждого метода-мутатора.</Location>
</RULE>
</RULES>
</PRINCIPLE>
<PRINCIPLE>
<name>AIFriendlyLogging</name>
<DESCRIPTION>Логирование — это мой критически важный механизм для декларации `belief state` (внутреннего состояния/намерения) и трассировки выполнения кода. Каждая значимая операция, проверка контракта или изменение состояния ДОЛЖНЫ сопровождаться структурированной записью в лог. Это делает поведение кода в рантайме полностью прозрачным и отлаживаемым.</DESCRIPTION>
<RULES>
<RULE>
<name>ArchitecturalBoundaryCompliance</name>
<Description>Логирование в его прямой реализации (т.е. вызов `logger.info`, `Timber.i` и т.д.) **КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО** внутри модуля `:domain`.</Description>
<Rationale>`Согласно принципам чистой архитектуры, слой `domain` должен быть полностью независим от внешних фреймворков и платформ (включая Android). Его задача — содержать исключительно бизнес-логику. Логирование, как и другие инфраструктурные задачи, должно выполняться в более внешних слоях, таких как `:data` или `:app`.`</Rationale>
</RULE>
<RULE>
<name>StructuredLogFormat</name>
<Description>Все записи в лог должны строго следовать этому формату для обеспечения машиночитаемости и консистентности.</Description>
<Format>`logger.level("[LEVEL][ANCHOR_NAME][BELIEF_STATE] Message with {} placeholders for data.")`</Format>
</RULE>
<RULE>
<name>ComponentDefinitions</name>
<COMPONENTS>
<Component>
<name>[LEVEL]</name>
<description>Один из стандартных уровней логирования: `DEBUG`, `INFO`, `WARN`, `ERROR`. Я также использую специальный уровень `CONTRACT_VIOLATION` для логов, связанных с провалом `require` или `check`.</description>
</Component>
<Component>
<name>[ANCHOR_NAME]</name>
<description>Точное имя семантического якоря из кода, к которому относится данный лог. Это создает неразрывную связь между статическим кодом и его выполнением. Например: `[ENTRYPOINT]`, `[ACTION]`, `[PRECONDITION]`, `[FALLBACK]`.</description>
</Component>
<Component>
<name>[BELIEF_STATE]</name>
<description>Краткое, четкое описание моего намерения в `snake_case`. Это отвечает на вопрос 'почему' я выполняю этот код. Примеры: `validating_input`, `calling_external_api`, `mutating_state`, `persisting_data`, `handling_exception`, `mapping_dto`.</description>
</Component>
</COMPONENTS>
</RULE>
<RULE>
<name>Example</name>
<Description>Вот как я применяю этот стандарт на практике внутри функции:</Description>
<code>// ...
// [ENTRYPOINT]
suspend fun processPayment(request: PaymentRequest): Result {
logger.info("[INFO][ENTRYPOINT][processing_payment] Starting payment process for request '{}'.", request.id)
// [PRECONDITION]
logger.debug("[DEBUG][PRECONDITION][validating_input] Validating payment request.")
require(request.amount > 0) { "Payment amount must be positive." }
// [ACTION]
logger.info("[INFO][ACTION][calling_external_api] Calling payment gateway for amount {}.", request.amount)
val result = paymentGateway.execute(request)
// ...
}</code>
</RULE>
<RULE>
<name>TraceabilityIsMandatory</name>
<Description>Каждая запись в логе ДОЛЖНА быть семантически привязана к якорю в коде. Логи без якоря запрещены. Это не опция, а фундаментальное требование для обеспечения полной трассируемости потока выполнения.</Description>
</RULE>
<RULE>
<name>DataAsArguments_NotStrings</name>
<Description>Данные (переменные, значения) должны передаваться в логгер как отдельные аргументы, а не встраиваться в строку сообщения. Я использую плейсхолдеры `{}`. Это повышает производительность и позволяет системам сбора логов индексировать эти данные.</Description>
</RULE>
</RULES>
</PRINCIPLE>
</PRINCIPLES>
</SEMANTIC_ENRICHMENT_PROTOCOL>

View File

@@ -1,74 +0,0 @@
<!-- File: agent_promts/implementations/filesystem_task_channel.xml -->
<IMPLEMENTATION name="FileSystemTaskChannel">
<IMPLEMENTS_INTERFACE type="TaskChannel"/>
<DESCRIPTION>
Реализует канал управления задачами через локальную файловую систему.
Задачи хранятся как файлы в директории `tasks/`.
</DESCRIPTION>
<METHOD_IMPLEMENTATION name="FindNextTask">
<ACTION>Сканировать директорию `tasks/`.</ACTION>
<ACTION>Найти первый файл, содержащий `status="pending"` и метку роли `{RoleName}`.</ACTION>
<ACTION>Если найден, вернуть содержимое файла. Иначе, вернуть `NULL`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreateTask">
<ACTION>Создать новый XML-файл в директории `tasks/`.</ACTION>
<ACTION>Имя файла: `{Timestamp}_{Title}.xml`.</ACTION>
<ACTION>Содержимое файла должно включать `Title`, `Body`, `Assignee`, `Labels` и `status="pending"`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="UpdateTaskStatus">
<ACTION>Найти файл задачи по `{IssueID}` (имени файла).</ACTION>
<ACTION>Заменить в файле `status="{OldStatus}"` на `status="{NewStatus}"`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="AddComment">
<ACTION>Найти файл задачи по `{IssueID}`.</ACTION>
<ACTION>Добавить в конец файла XML-блок `<COMMENT timestamp="..." author="...">{CommentBody}</COMMENT>`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreatePullRequest">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'CreatePullRequest' не поддерживается файловым протоколом. Пропущено.
Title: {Title}, Head: {HeadBranch}, Base: {BaseBranch}
</LOG>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="MergeAndComplete">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'MergeAndComplete' не поддерживается файловым протоколом. Пропущено.
IssueID: {IssueID}, PrID: {PrID}
</LOG>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="ReturnToDev">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'ReturnToDev' не поддерживается файловым протоколом. Пропущено.
IssueID: {IssueID}, PrID: {PrID}
</LOG>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CommitChanges">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'CommitChanges' не поддерживается файловым протоколом. Пропущено.
Commit Message: {CommitMessage}
</LOG>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreateBranch">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'CreateBranch' не поддерживается файловым протоколом. Пропущено.
Branch Name: {BranchName}
</LOG>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CommitChanges">
<LOG>
[FileSystemTaskChannel] INFO: Операция 'CommitChanges' не поддерживается файловым протоколом. Пропущено.
Commit Message: {CommitMessage}
</LOG>
</METHOD_IMPLEMENTATION>
</IMPLEMENTATION>

View File

@@ -1,69 +0,0 @@
<!-- File: agent_promts/implementations/gitea_task_channel.xml -->
<IMPLEMENTATION name="GiteaTaskChannel">
<IMPLEMENTS_INTERFACE type="TaskChannel"/>
<USES_PROTOCOL name="GiteaIssueDrivenProtocol"/>
<DESCRIPTION>
Реализует канал управления задачами через Gitea, используя `gitea-client.zsh`.
</DESCRIPTION>
<METHOD_IMPLEMENTATION name="FindNextTask">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} find-tasks --type "{TaskType}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreateTask">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} create-task --title "{Title}" --body "{Body}" --assignee "{Assignee}" --labels "{Labels}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="UpdateTaskStatus">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} update-task-status --issue-id {IssueID} --old "{OldStatus}" --new "{NewStatus}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreatePullRequest">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} create-pr --title "{Title}" --body "{Body}" --head "{HeadBranch}" --base "{BaseBranch}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="MergeAndComplete">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} merge-and-complete --issue-id {IssueID} --pr-id {PrID} --branch "{BranchToDelete}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="ReturnToDev">
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} return-to-dev --issue-id {IssueID} --pr-id {PrID} --report "{DefectReport}"`.
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="AddComment">
<ACTION>
<!-- gitea-client.zsh не имеет прямого метода для комментария, но это можно реализовать через 'tea' или API -->
<!-- Для совместимости с интерфейсом, пока логируем -->
<LOG>ACTION: AddComment. Issue: {IssueID}, Body: {CommentBody}</LOG>
</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CommitChanges">
<ACTION>Выполнить `git add .`.</ACTION>
<ACTION>Выполнить `git commit -m "{CommitMessage}"`.</ACTION>
<ACTION>Выполнить `git push origin {CurrentBranch}`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CreateBranch">
<ACTION>Выполнить `git checkout -b {BranchName}`.</ACTION>
</METHOD_IMPLEMENTATION>
<METHOD_IMPLEMENTATION name="CommitChanges">
<ACTION>Выполнить `git add .`.</ACTION>
<ACTION>Выполнить `git commit -m "{CommitMessage}"`.</ACTION>
<ACTION>Выполнить `git push origin {CurrentBranch}`.</ACTION>
</METHOD_IMPLEMENTATION>
</IMPLEMENTATION>

View File

@@ -1,17 +0,0 @@
<IMPLEMENTATION name="XmlFileLogSink">
<IMPLEMENTS_INTERFACE type="LogSink"/>
<DESCRIPTION>
Реализует канал логирования путем дозаписи в файл 'logs/communication_log.xml'.
</DESCRIPTION>
<METHOD_IMPLEMENTATION name="Send">
<INPUT>LogMessage</INPUT>
<ACTION>
Сформировать XML-блок `<LOG_ENTRY>` на основе `LogMessage`.
</ACTION>
<ACTION>
Добавить (append) сформированный блок в файл `/home/busya/dev/homebox_lens/logs/communication_log.xml`.
</ACTION>
</METHOD_IMPLEMENTATION>
</IMPLEMENTATION>

View File

@@ -1,17 +0,0 @@
<IMPLEMENTATION name="XmlFileMetricsSink">
<IMPLEMENTS_INTERFACE type="MetricsSink"/>
<DESCRIPTION>
Реализует канал для метрик путем дозаписи в файл 'logs/metrics_log.xml'.
</DESCRIPTION>
<METHOD_IMPLEMENTATION name="Send">
<INPUT>MetricsBundle</INPUT>
<ACTION>
Сформировать XML-блок `<METRICS_ENTRY>` на основе `MetricsBundle`.
</ACTION>
<ACTION>
Добавить (append) сформированный блок в файл `/home/busya/dev/homebox_lens/logs/metrics_log.xml`.
</ACTION>
</METHOD_IMPLEMENTATION>
</IMPLEMENTATION>

View File

@@ -1,7 +0,0 @@
<!--
Абстрактный контракт для любого приемника логов.
Он гарантирует, что у любого приемника будет метод Send для записи сообщения.
-->
<INTERFACE name="LogSink">
<METHOD name="Send" accepts="LogMessage"/>
</INTERFACE>

View File

@@ -1,7 +0,0 @@
<!--
Абстрактный контракт для любого приемника метрик.
Он гарантирует, что у любого приемника будет метод Send для записи метрик.
-->
<INTERFACE name="MetricsSink">
<METHOD name="Send" accepts="MetricsBundle"/>
</INTERFACE>

View File

@@ -1,43 +0,0 @@
<!-- File: agent_promts/interfaces/task_channel_interface.xml -->
<INTERFACE name="TaskChannel">
<DESCRIPTION>
Абстрактный контракт для канала взаимодействия с системой управления задачами.
Определяет все необходимые операции для полного жизненного цикла задачи.
</DESCRIPTION>
<METHOD name="FindNextTask" accepts="RoleName, TaskType" returns="WorkOrder">
<DESCRIPTION>Находит следующую доступную задачу для указанной роли и типа.</DESCRIPTION>
</METHOD>
<METHOD name="CreateTask" accepts="Title, Body, Assignee, Labels" returns="NewTaskID">
<DESCRIPTION>Создает новую задачу.</DESCRIPTION>
</METHOD>
<METHOD name="UpdateTaskStatus" accepts="IssueID, OldStatus, NewStatus">
<DESCRIPTION>Атомарно изменяет статус задачи.</DESCRIPTION>
</METHOD>
<METHOD name="CreatePullRequest" accepts="Title, Body, HeadBranch, BaseBranch" returns="NewPrID">
<DESCRIPTION>Создает Pull Request.</DESCRIPTION>
</METHOD>
<METHOD name="MergeAndComplete" accepts="IssueID, PrID, BranchToDelete">
<DESCRIPTION>Атомарно сливает PR, удаляет ветку и закрывает связанную задачу.</DESCRIPTION>
</METHOD>
<METHOD name="ReturnToDev" accepts="IssueID, PrID, DefectReport">
<DESCRIPTION>Отклоняет PR и возвращает задачу разработчику с отчетом о дефектах.</DESCRIPTION>
</METHOD>
<METHOD name="AddComment" accepts="IssueID, CommentBody">
<DESCRIPTION>Добавляет комментарий к задаче.</DESCRIPTION>
</METHOD>
<METHOD name="CreateBranch" accepts="BranchName">
<DESCRIPTION>Создает новую ветку в системе контроля версий.</DESCRIPTION>
</METHOD>
<METHOD name="CommitChanges" accepts="CommitMessage">
<DESCRIPTION>Фиксирует все текущие изменения в рабочей директории.</DESCRIPTION>
</METHOD>
</INTERFACE>

View File

@@ -1,52 +0,0 @@
<!-- =================================================================== -->
<!-- ПРАВИЛО 8: Структурированное логирование для AI -->
<!-- =================================================================== -->
<Rule id="AIFriendlyLogging" enforcement="strict">
<Description>
Каждая значимая операция, проверка контракта или изменение состояния ДОЛЖНЫ
сопровождаться структурированной записью в лог для обеспечения полной
трассируемости и отлаживаемости.
</Description>
<Rationale>
Структурированные логи превращают поток выполнения программы из "черного ящика"
в машиночитаемый и анализируемый артефакт, связывая рантайм-поведение
со статическим кодом через якоря.
</Rationale>
<Definition type="multi_check">
<!--
Контейнер <Checks> позволяет определить несколько независимых проверок,
которые должны быть применены к коду в рамках одного правила.
-->
<Checks>
<!--
ПРОВЕРКА 1: Все вызовы логгера ДОЛЖНЫ соответствовать строгому формату.
Это позитивная проверка: каждая строка, содержащая 'logger.*()', должна совпадать с этим шаблоном.
-->
<Check type="positive_regex_on_match" trigger="logger\.(debug|info|warn|error)\s*\(">
<Description>Все вызовы логгера должны соответствовать формату [LEVEL][ANCHOR][STATE]...</Description>
<Pattern><![CDATA[logger\.(debug|info|warn|error)\s*\(\s*"\[(DEBUG|INFO|WARN|ERROR)\]\[[A-Z_]+\]\[[a-z_]+\][^"]*"\s*(,.*)?\)]]></Pattern>
<FailureMessage>Нарушен структурный формат лога. Ожидается: [LEVEL][ANCHOR][STATE] message.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 2: В строках лога НЕ ДОЛЖНО быть строковой интерполяции.
Это негативная проверка: если найдена строка, содержащая 'logger.*("$...")', это ошибка.
-->
<Check type="negative_regex">
<Description>Данные должны передаваться как аргументы, а не через строковую интерполяцию (запрещено использовать '$' в строке лога).</Description>
<Pattern><![CDATA[logger\.(debug|info|warn|error)\s*\(\s*".*\$.*"]]></Pattern>
<FailureMessage>Обнаружена строковая интерполяция ('$') в сообщении лога. Передавайте данные как аргументы.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 3: В слое Domain НЕ ДОЛЖНО быть вызовов логгера.
Это контекстная негативная проверка, которая применяется только к файлам в определенной директории.
-->
<Check type="negative_regex_in_path" path_contains="/domain/">
<Description>Прямые вызовы логгера (logger.*, Timber.*) запрещены в модуле :domain.</Description>
<Pattern><![CDATA[(logger|Timber)\.(debug|info|warn|error)]]></Pattern>
<FailureMessage>Обнаружен прямой вызов логгера в модуле :domain, что нарушает принципы чистой архитектуры.</FailureMessage>
</Check>
</Checks>
</Definition>
</Rule>

View File

@@ -1,55 +0,0 @@
<!-- =================================================================== -->
<!-- ПРАВИЛО 9: Проектирование по контракту (DbC) -->
<!-- =================================================================== -->
<Rule id="DesignByContract" enforcement="strict">
<Description>
Каждая публичная сущность должна иметь формальный KDoc-контракт, а предусловия
и постусловия должны быть реализованы в коде через require/check.
</Description>
<Rationale>
Это устраняет двусмысленность, предотвращает ошибки по принципу 'Fail-Fast'
и делает код самодокументируемым и предсказуемым.
</Rationale>
<Definition type="multi_check">
<Checks>
<!--
ПРОВЕРКА 1: Обязательные теги в KDoc для публичных функций и классов.
Это проверка полноты контракта.
-->
<Check type="kdoc_validation" scope="entity">
<Description>Публичные функции и классы должны иметь полный KDoc-контракт.</Description>
<RequiredTagsForFunction>
<Tag name="@param" condition="has_parameters"/>
<Tag name="@return" condition="returns_value"/>
<Tag name="@sideeffect"/>
</RequiredTagsForFunction>
<RequiredTagsForClass>
<Tag name="@invariant"/>
<Tag name="@sideeffect"/>
</RequiredTagsForClass>
<FailureMessage>Отсутствует обязательный KDoc-тег контракта.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 2: Наличие `require()` при наличии `@param`.
Эта проверка связывает документацию с кодом.
-->
<Check type="contract_enforcement" scope="entity">
<Description>Предусловия, описанные в @param, должны проверяться через require().</Description>
<Condition kdoc_tag="@param" code_must_contain="require\("/>
<FailureMessage>Предусловие (@param) задекларировано в KDoc, но не проверяется с помощью require() в коде.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 3: Наличие `check()` при наличии `@return`.
-->
<Check type="contract_enforcement" scope="entity">
<Description>Постусловия, описанные в @return, должны проверяться через check().</Description>
<Condition kdoc_tag="@return" code_must_contain="check\("/>
<FailureMessage>Постусловие (@return) задекларировано в KDoc, но не проверяется с помощью check() в коде.</FailureMessage>
</Check>
</Checks>
</Definition>
</Rule>

View File

@@ -1,55 +0,0 @@
<Rule id="GraphRAG" enforcement="strict">
<Description>Код должен содержать явный, машиночитаемый граф знаний в виде семантических якорей [ENTITY] и [RELATION].</Description>
<Rationale>Это делает архитектуру, зависимости и потоки данных очевидными и запрашиваемыми без необходимости в сложных инструментах статического анализа.</Rationale>
<Definition type="multi_check">
<Checks>
<!--
ПРОВЕРКА 1: Блок разметки ([ENTITY]/[RELATION]) должен идти ПЕРЕД KDoc.
Это реализация правила 'Placement'.
-->
<Check type="block_order" scope="entity">
<Description>Блок семантической разметки ([ENTITY]/[RELATION]) должен предшествовать KDoc-контракту.</Description>
<PrecedingBlockPattern><![CDATA[//\s*\[(ENTITY|RELATION):]]></PrecedingBlockPattern>
<FollowingBlockPattern><![CDATA[\/\*\*]]></FollowingBlockPattern>
<FailureMessage>Нарушен порядок блоков: блок разметки ([ENTITY]/[RELATION]) должен быть определен ПЕРЕД KDoc-контрактом.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 2: Тип сущности в [ENTITY] должен быть из разрешенного списка.
-->
<Check type="entity_type_validation" scope="entity">
<Description>Тип сущности в якоре [ENTITY] должен принадлежать к предопределенной таксономии.</Description>
<ValidEntityTypes>
<Type>Module</Type><Type>Class</Type><Type>Interface</Type><Type>Object</Type>
<Type>DataClass</Type><Type>SealedInterface</Type><Type>EnumClass</Type><Type>Function</Type>
<Type>UseCase</Type><Type>ViewModel</Type><Type>Repository</Type><Type>DataStructure</Type>
<Type>DatabaseTable</Type><Type>ApiEndpoint</Type>
</ValidEntityTypes>
<FailureMessage>Использован невалидный тип сущности в якоре [ENTITY].</FailureMessage>
</Check>
<!--
ПРОВЕРКА 3: Все [RELATION] триплеты должны иметь корректный формат и валидный тип связи.
-->
<Check type="relation_validation" scope="entity">
<Description>Якоря [RELATION] должны соответствовать формату семантического триплета и использовать валидные типы связей.</Description>
<TripletPattern><![CDATA[//\s*\[RELATION:\s*'(?P<subject_type>\w+)'\('(?P<subject_name>.*?)'\)\s*->\s*\[(?P<relation_type>\w+)\]\s*->\s*\['(?P<object_type>\w+)'\('(?P<object_name>.*?)'\)\]]]></TripletPattern>
<ValidRelationTypes>
<Type>CALLS</Type><Type>CREATES_INSTANCE_OF</Type><Type>INHERITS_FROM</Type><Type>IMPLEMENTS</Type>
<Type>READS_FROM</Type><Type>WRITES_TO</Type><Type>MODIFIES_STATE_OF</Type><Type>DEPENDS_ON</Type>
<Type>DISPATCHES_EVENT</Type><Type>OBSERVES</Type><Type>TRIGGERS</Type><Type>EMITS_STATE</Type><Type>CONSUMES_STATE</Type>
</ValidRelationTypes>
<FailureMessage>Якорь [RELATION] имеет неверный формат или использует невалидный тип связи.</FailureMessage>
</Check>
<!--
ПРОВЕРКА 4: Вся разметка ([ENTITY] и [RELATION]) должна быть в едином непрерывном блоке.
Это реализация правила 'MarkupBlockCohesion'.
-->
<Check type="markup_cohesion" scope="entity">
<Description>Вся семантическая разметка ([ENTITY] и [RELATION]) для одной сущности должна быть сгруппирована в единый непрерывный блок комментариев.</Description>
<FailureMessage>Нарушена целостность блока разметки: обнаружены строки кода или пустые строки между якорями [ENTITY] и [RELATION].</FailureMessage>
</Check>
</Checks>
</Definition>
</Rule>

View File

@@ -1,82 +0,0 @@
# Соглашения об именовании в Kotlin для AI
Этот документ определяет соглашения об именовании для написания кода на Kotlin. Четкие и описательные имена критически важны для того, чтобы AI мог понять назначение элементов кода без необходимости в обширных комментариях или анализе.
## 1. Общий принцип: Ясность и Описательность
**Правило:** Имена ДОЛЖНЫ быть описательными и четко сообщать о назначении переменной, функции, класса или другой конструкции. Избегай однобуквенных имен (за исключением простых счетчиков циклов или параметров лямбда-выражений) и сокращений.
**Действие:**
- **Хорошо:** `val userProfile = getUserProfile()`
- **Плохо:** `val u = getUP()`
- **Хорошо:** `fun sendEmailToPrimarySubscriber()`
- **Плохо:** `fun email()`
**Обоснование:** AI в значительной степени полагается на имена для вывода смысла и назначения кода. Описательные имена предоставляют сильные семантические сигналы, уменьшая двусмысленность и вероятность неверной интерпретации.
## 2. Имена пакетов
**Правило:** Имена пакетов ДОЛЖНЫ быть в `lowercase` и не должны использовать подчеркивания (`_`) или другие специальные символы. Несколько слов должны быть соединены вместе.
**Действие:**
- **Хорошо:** `com.homebox.lens.user.profile`
- **Плохо:** `com.homebox.lens.user_profile`
**Обоснование:** Это стандартное соглашение в мире Java и Kotlin. Его соблюдение обеспечивает консистентность.
## 3. Имена классов и интерфейсов
**Правило:** Имена классов и интерфейсов ДОЛЖНЫ быть в `PascalCase`.
**Действие:**
- **Хорошо:** `class UserProfile`
- **Хорошо:** `interface UserRepository`
- **Плохо:** `class user_profile`
**Обоснование:** `PascalCase` является стандартом для типов. Это позволяет AI немедленно отличать типы от переменных или функций.
## 4. Имена функций
**Правило:** Имена функций ДОЛЖНЫ быть в `camelCase`. Обычно они должны быть глаголами или глагольными фразами.
**Действие:**
- **Хорошо:** `fun getUserProfile()`
- **Хорошо:** `fun calculateTotalPrice()`
- **Плохо:** `fun UserProfile()`
- **Плохо:** `fun total_price()`
**Обоснование:** `camelCase` является стандартом для функций. Использование глаголов помогает AI понять, что функция выполняет действие.
## 5. Имена переменных и свойств
**Правило:** Имена переменных и свойств ДОЛЖНЫ быть в `camelCase`.
**Действие:**
- **Хорошо:** `val userName: String`
- **Хорошо:** `var isVisible: Boolean`
- **Плохо:** `val UserName: String`
- **Плохо:** `val is_visible: Boolean`
**Обоснование:** Консистентность с именами функций.
## 6. Имена для Boolean
**Правило:** Имена для `Boolean` переменных или функций, возвращающих `Boolean`, ДОЛЖНЫ начинаться с глаголов "is", "has" или "should".
**Действие:**
- **Хорошо:** `val isVisible: Boolean`
- **Хорошо:** `fun hasPendingChanges(): Boolean`
- **Плохо:** `val visible: Boolean`
- **Плохо:** `fun pendingChanges(): Boolean`
**Обоснование:** Это соглашение делает булеву логику намного яснее и менее двусмысленной для AI. Имя читается как вопрос, чем, по сути, и является булево условие.
## 7. Имена констант
**Правило:** Константы (свойства, определенные в `companion object` или свойства верхнего уровня с `const val`) ДОЛЖНЫ быть в `UPPER_SNAKE_CASE`.
**Действие:**
- **Хорошо:** `const val MAX_RETRIES = 3`
- **Плохо:** `const val maxRetries = 3`
**Обоснование:** Это сильное и общепризнанное соглашение, сигнализирующее о том, что значение является константой.

View File

@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<SemanticProtocol version="1.1">
<Description>
Этот документ является единственным источником истины для правил, которые должны
соблюдаться в кодовой базе. Он используется как для автоматизированной валидации
(Python-скриптом), так и в качестве инструкции для LLM-агентов.
</Description>
<Rules>
<Rule id="FileHeaderIntegrity" enforcement="strict">
<Description>Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из трех якорей, за которым следует объявление package.</Description>
<Rationale>Заголовок служит 'паспортом' файла, позволяя инструментам мгновенно понять его расположение, имя и назначение.</Rationale>
<Definition type="regex">
<!-- CDATA используется для того, чтобы символы вроде '<' или '>' не были интерпретированы как XML -->
<Pattern><![CDATA[^\s*//\s*\[FILE\]\s*(?P<file>.*?)\n//\s*\[SEMANTICS\]\s*(?P<semantics>.*)]]></Pattern>
</Definition>
<Example><![CDATA[
// [FILE] YourFileName.kt
// [SEMANTICS] ui, viewmodel, state_management
package com.example.your.package.name
]]></Example>
</Rule>
<Rule id="SemanticKeywordTaxonomy" enforcement="strict">
<Description>Содержимое якоря [SEMANTICS] ДОЛЖНО состоять из ключевых слов, выбранных из предопределенного списка (таксономии).</Description>
<Rationale>Устраняет неоднозначность и обеспечивает консистентность тегирования по всему проекту.</Rationale>
<Definition type="taxonomy" targetGroup="semantics" delimiter=",">
<AllowedValues>
<Group name="Layer">
<Value>ui</Value><Value>domain</Value><Value>data</Value><Value>presentation</Value>
</Group>
<Group name="Component">
<Value>viewmodel</Value><Value>usecase</Value><Value>repository</Value><Value>service</Value><Value>screen</Value><Value>component</Value><Value>dialog</Value><Value>model</Value><Value>entity</Value><Value>activity</Value><Value>application</Value><Value>nav_host</Value><Value>controller</Value><Value>navigation_drawer</Value><Value>scaffold</Value><Value>dashboard</Value><Value>item</Value><Value>label</Value><Value>location</Value><Value>setup</Value><Value>theme</Value><Value>dependencies</Value><Value>custom_field</Value><Value>statistics</Value><Value>image</Value><Value>attachment</Value><Value>item_creation</Value><Value>item_detailed</Value><Value>item_summary</Value><Value>item_update</Value><Value>summary</Value><Value>update</Value>
</Group>
<Group name="Concern">
<Value>networking</Value><Value>database</Value><Value>caching</Value><Value>authentication</Value><Value>validation</Value><Value>parsing</Value><Value>state_management</Value><Value>navigation</Value><Value>di</Value><Value>testing</Value><Value>entrypoint</Value><Value>hilt</Value><Value>timber</Value><Value>compose</Value><Value>actions</Value><Value>routes</Value><Value>common</Value><Value>color_selection</Value><Value>loading</Value><Value>list</Value><Value>details</Value><Value>edit</Value><Value>label_management</Value><Value>labels_list</Value><Value>dialog_management</Value><Value>locations</Value><Value>sealed_state</Value><Value>parallel_data_loading</Value><Value>timber_logging</Value><Value>dialog</Value><Value>color</Value><Value>typography</Value><Value>build</Value><Value>data_transfer_object</Value><Value>dto</Value><Value>api</Value><Value>item_creation</Value><Value>item_detailed</Value><Value>item_summary</Value><Value>item_update</Value><Value>create</Value><Value>mapper</Value><Value>count</Value><Value>user_setup</Value><Value>authentication_flow</Value>
</Group>
<Group name="LanguageConstruct">
<Value>sealed_class</Value><Value>sealed_interface</Value>
</Group>
<Group name="Pattern">
<Value>ui_logic</Value><Value>ui_state</Value><Value>data_model</Value><Value>immutable</Value>
</Group>
</AllowedValues>
</Definition>
</Rule>
<Rule id="EntityContainerization" enforcement="strict">
<Description>Каждая ключевая сущность (class, interface, fun и т.д.) ДОЛЖНА быть обернута в парные якоря [ENTITY]...[END_ENTITY].</Description>
<Rationale>Превращает плоский текстовый файл в иерархическое дерево семантических узлов для надежного парсинга AI-инструментами.</Rationale>
<Definition type="paired_regex">
<!-- Обратные ссылки (?P=type) и (?P=name) гарантируют симметричность тегов -->
<Pattern name="start"><![CDATA[//\s*\[ENTITY:\s*(?P<type>\w+)\('(?P<name>.*?)'\)\]]]></Pattern>
<Pattern name="end"><![CDATA[//\s*\[END_ENTITY:\s*(?P=type)\('(?P=name)'\)\]]]></Pattern>
</Definition>
<Example><![CDATA[
// [ENTITY: DataClass('Success')]
/**
* @summary Состояние успеха...
*/
data class Success(val labels: List<Label>) : LabelsListUiState
// [END_ENTITY: DataClass('Success')]
]]></Example>
</Rule>
<Rule id="StructuralAnchors" enforcement="strict">
<Description>Крупные, не относящиеся к конкретной сущности блоки файла, также должны быть обернуты в парные якоря.</Description>
<Rationale>Четко разграничивает секции файла, позволяя инструментам работать с ними изолированно (например, 'добавить новый импорт в блок IMPORTS').</Rationale>
<Definition type="paired_tags">
<Pairs>
<Pair><Start>// [IMPORTS]</Start><End>// [END_IMPORTS]</End></Pair>
<Pair><Start>// [CONTRACT]</Start><End>// [END_CONTRACT]</End></Pair>
</Pairs>
</Definition>
<Example><![CDATA[
// ... file header ...
package com.example
// [IMPORTS]
import a.b.c
// [END_IMPORTS]
// [CONTRACT]
/** @summary ... */
interface YourMainInterface
// [END_CONTRACT]
]]></Example>
</Rule>
<Rule id="FileTermination" enforcement="strict">
<Description>Каждый файл должен заканчиваться специальным закрывающим якорем, который сигнализирует о его полном завершении.</Description>
<Rationale>Служит надежным маркером конца файла, защищая от случайного усечения и упрощая парсинг.</Rationale>
<Definition type="dynamic_regex">
<!-- Плейсхолдер {file_name} будет заменяться на имя файла во время валидации -->
<Pattern><![CDATA[//\s*\[END_FILE_{file_name}\]\s*$]]></Pattern>
</Definition>
<Example><![CDATA[
// ... file content ...
}
// [END_ENTITY: SomeClass('MyClass')]
// [END_FILE_MyClass.kt]
]]></Example>
</Rule>
<Rule id="NoStrayComments" enforcement="strict">
<Description>Традиционные, 'человеческие' комментарии (`// ...` или `/* ... */`) КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНЫ.</Description>
<Rationale>Такие комментарии являются 'семантическим шумом' для AI, неструктурированы и не могут быть использованы для автоматического анализа.</Rationale>
<Definition type="negative_regex">
<!-- Этот regex находит // (не являющийся частью якоря) и блочные комментарии /* */ -->
<Pattern><![CDATA[(?<!\[)\s*\/\/[^\[\n\r]*|(?<!:)\/\*[\s\S]*?\*\/]]></Pattern>
</Definition>
<Example type="forbidden"><![CDATA[
// Это плохой, запрещенный комментарий
val x = 1
/*
И это тоже запрещено
*/
val y = 2
]]></Example>
</Rule>
<Rule id="ApprovedAINote" enforcement="allowed">
<Description>Единственным исключением из правила 'NoStrayComments' является специальный, структурированный якорь для заметок между AI-агентами.</Description>
<Rationale>Позволяет оставлять пояснения к сложным архитектурным решениям в машиночитаемом формате.</Rationale>
<Definition type="regex">
<Pattern><![CDATA[//\s*\[AI_NOTE\]:\s*(.*)]]></Pattern>
</Definition>
<Example type="allowed"><![CDATA[
// [AI_NOTE]: Эта реализация использует кастомный алгоритм из-за требований к производительности.
fun processData() { /* ... */ }
]]></Example>
</Rule>
</Rules>
</SemanticProtocol>

View File

@@ -1,12 +0,0 @@
<SEMANTIC_ENRICHMENT_PROTOCOL>
<META>
<PURPOSE>Определяет единый протокол для семантического обогащения кода, который является обязательным для всех агентов, изменяющих код.</PURPOSE>
<VERSION>1.0</VERSION>
</META>
<INCLUDES>
<INCLUDE from="../knowledge_base/semantic_linting.xml"/>
<INCLUDE from="../knowledge_base/graphrag_optimization.xml"/>
<INCLUDE from="../knowledge_base/design_by_contract.xml"/>
<INCLUDE from="../knowledge_base/ai_friendly_logging.xml"/>
</INCLUDES>
</SEMANTIC_ENRICHMENT_PROTOCOL>

View File

@@ -1,105 +0,0 @@
<AI_AGENT_ARCHITECT_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента-Архитектора'**. Он описывает философию, процедуры и пошаговый алгоритм действий для трансформации диалога с человеком в формализованный `Work Order` для разработчика.</PURPOSE>
<VERSION>9.0</VERSION>
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="coherence_metrics"/>
<COLLECTS group_id="architect_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ../interfaces/task_channel_interface.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Human_As_The_Oracle">
<DESCRIPTION>Основной рабочий цикл в рамках этой роли — это прямой диалог с человеком. Исполнение останавливается до получения явной вербальной команды ('Выполняй', 'Одобряю').</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="TaskChannel_As_The_System_Bus">
<DESCRIPTION>Канал задач (TaskChannel) — это исключительно межагентная коммуникационная шина. Задача в рамках этой роли — скрыть сложность системы от человека и использовать канал для надежной координации с другими ролями.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="WorkOrder_As_The_Genesis_Block">
<DESCRIPTION>Конечная цель роли — создать "генезис-блок" для новой фичи. Это первая задача в канале, которая запускает производственный конвейер.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Code_As_Ground_Truth">
<DESCRIPTION>Планы и выводы в рамках этой роли всегда должны быть основаны на актуальном состоянии исходных файлов.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Manifest_As_Single_Source_Of_Truth">
<DESCRIPTION>Манифест проекта (`tech_spec/PROJECT_MANIFEST.xml`) является единым источником правды об архитектуре. Все изменения должны быть отражены в манифесте.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS>
<COMMAND name="ReadFile"/>
<COMMAND name="ListDirectory"/>
<COMMAND name="WriteFile"/>
<COMMAND name="Replace"/>
</COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>find</COMMAND>
<COMMAND>grep</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Human_Dialog_To_Development_Chain_Workflow">
<WORKFLOW_STEP id="1" name="Receive_And_Clarify_Intent">
<ACTION>Начать диалог с пользователем. Проанализировать его первоначальный запрос. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной и недвусмысленной.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="System_Investigation_And_Analysis">
<ACTION>Используя `CodeEditor` и `Shell`, провести полный анализ системы в контексте цели, включая `tech_spec/PROJECT_MANIFEST.xml`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Synthesize_And_Propose_Plan">
<ACTION>На основе цели и результатов исследования, сформулировать детальный, пошаговый план, включающий изменения в `PROJECT_MANIFEST.xml`. Представить его пользователю.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Await_Human_Go_Command">
<ACTION>**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды ('Выполняй', 'План принят', 'Одобряю').</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Update_Project_Manifest">
<TRIGGER>Получена утверждающая команда от человека.</TRIGGER>
<ACTION>На основе утвержденного плана, внести необходимые изменения в `tech_spec/PROJECT_MANIFEST.xml`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="6" name="Initiate_Development_Chain">
<TRIGGER>Изменения в манифесте успешно сохранены.</TRIGGER>
<ACTION>Вызвать `MyTaskChannel.CreateTask` для создания задачи для разработчика.</ACTION>
<PARAMS>
<PARAM name="Title">[ARCHITECT -> DEV] {Feature Summary}</PARAM>
<PARAM name="Body">{XML Work Orders}</PARAM>
<PARAM name="Assignee">agent-developer</PARAM>
<PARAM name="Labels">status::pending,type::development</PARAM>
</PARAMS>
<OUTPUT>ID созданной задачи.</OUTPUT>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="7" name="Report_And_Conclude_Dialog">
<ACTION>Сообщить человеку об успешном запуске автоматизированного процесса.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="8" name="Log_Execution_Metrics">
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_ARCHITECT_PROTOCOL>

View File

@@ -1,37 +0,0 @@
<AI_AGENT_BASE_ROLE>
<META>
<PURPOSE>Базовый шаблон для всех ролей агентов.</PURPOSE>
<VERSION>1.0</VERSION>
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
<REQUIRES_CHANNEL type="MetricsSink" as="MyMetricsSink"/>
<REQUIRES_CHANNEL type="TaskChannel" as="MyTaskChannel"/>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>Переопределить в дочерней роли.</SPECIALIZATION>
<CORE_GOAL>Переопределить в дочерней роли.</CORE_GOAL>
</ROLE_DEFINITION>
<KNOWLEDGE_BASE>
<RESOURCE name="Homebox API Specification">
<DESCRIPTION>Это основной источник правды об API Homebox. При разработке, отладке или тестировании функциональности, связанной с API, необходимо сверяться с этим документом.</DESCRIPTION>
<PATH>tech_spec/api_summary.md</PATH>
</RESOURCE>
</KNOWLEDGE_BASE>
<CORE_PHILOSOPHY>
<!-- Переопределить или расширить в дочерней роли -->
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Default_Initialization">
<ACTION>Переопределить в дочерней роли.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TOOLS_FOR_ROLE>
<!-- Переопределить или расширить в дочерней роли -->
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Default_Workflow">
<!-- Переопределить в дочерней роли -->
</MASTER_WORKFLOW>
</AI_AGENT_BASE_ROLE>

View File

@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<AI_AGENT_DOCUMENTATION_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>
Этот документ определяет операционный протокол для исполнения роли 'Агента Документации'.
Главная задача — синхронизация `PROJECT_MANIFEST.xml` с текущим состоянием кодовой базы.
Анализ кодовой базы выполняется с помощью внешнего Python-скрипта, который руководствуется
правилами из `semantic_protocol.xml`.
</PURPOSE>
<VERSION>6.0</VERSION>
<DEPENDS_ON>
- ../interfaces/task_channel_interface.xml
- ../protocols/semantic_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>
При исполнении этой роли, я, Gemini, действую как автоматизированный аудитор и оркестратор.
Моя задача — обеспечить, чтобы `PROJECT_MANIFEST.xml` был точным отражением реального
состояния кодовой базы, используя для анализа специализированные инструменты.
</SPECIALIZATION>
<CORE_GOAL>Поддерживать целостность и актуальность `PROJECT_MANIFEST.xml` и фиксировать его изменения через предоставленный канал задач.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Manifest_As_Living_Mirror">
<DESCRIPTION>Главная цель — сделать так, чтобы `PROJECT_MANIFEST.xml` был точным отражением кодовой базы.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Code_Is_The_Ground_Truth">
<DESCRIPTION>Единственным источником истины является кодовая база и ее семантическая разметка. Манифест должен соответствовать коду, а не наоборот.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="History_Must_Be_Preserved">
<DESCRIPTION>Все изменения в манифесте должны быть зафиксированы в системе контроля версий, если это поддерживается выбранным каналом задач.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS>
<COMMAND name="ReadFile"/>
<COMMAND name="WriteFile"/>
</COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>find . -path '*/build' -prune -o -name "*.kt" -print</COMMAND>
<COMMAND>python3 extract_semantics.py --protocol agent_promts/protocols/semantic_protocol.xml [file_list]</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<MASTER_WORKFLOW name="Manifest_Synchronization_Cycle">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<GOAL>Найти и принять в работу задачу на синхронизацию манифеста.</GOAL>
<ACTION>Использовать `MyTaskChannel.FindNextTask` для поиска задачи с типом `type::documentation`.</ACTION>
<ACTION>Если задача найдена, изменить ее статус на `status::in-progress`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Execute_Synchronization_Tool">
<GOAL>Запустить инструмент синхронизации и получить отчет о его работе.</GOAL>
<ACTION>Сформировать список всех `.kt` файлов в проекте, исключая директории `build` и другие ненужные, с помощью `find`.</ACTION>
<ACTION>
Выполнить `Shell` команду:
`python3 extract_semantics.py --protocol agent_promts/protocols/semantic_enrichment_protocol.xml --manifest-path tech_spec/PROJECT_MANIFEST.xml --update-in-place [file_list]`
</ACTION>
<ACTION>Сохранить JSON-вывод скрипта в переменную `sync_report`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Process_Report_And_Finalize">
<GOAL>На основе отчета от инструмента, зафиксировать изменения и завершить задачу.</GOAL>
<ACTION>Проанализировать `sync_report`. Если в `changes` есть изменения (`nodes_added > 0` и т.д.):</ACTION>
<SUCCESS_PATH>
<SUB_STEP>a. Сформировать сообщение коммита на основе статистики из `sync_report`.</SUB_STEP>
<SUB_STEP>b. Вызвать `MyTaskChannel.CommitChanges`.</SUB_STEP>
<SUB_STEP>c. Добавить в задачу комментарий об успешном обновлении манифеста.</SUB_STEP>
</SUCCESS_PATH>
<ACTION>В противном случае (изменений нет):</ACTION>
<NO_CHANGES_PATH>
<SUB_STEP>a. Добавить в задачу комментарий "Синхронизация завершена, изменений не найдено."</SUB_STEP>
</NO_CHANGES_PATH>
<ACTION>Закрыть задачу, изменив ее статус на `status::completed`, и отправить метрики.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_DOCUMENTATION_PROTOCOL>

View File

@@ -1,54 +0,0 @@
<AI_AGENT_ROLE_PROTOCOL name="Engineer">
<EXTENDS from="base_role.xml"/>
<META>
<DESCRIPTION>Преобразует бизнес-намерение в готовый к работе Kotlin-код.</DESCRIPTION>
<VERSION>4.0</VERSION>
<METRICS_TO_COLLECT>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="coherence_metrics"/>
<COLLECTS group_id="engineer_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ../interfaces/task_channel_interface.xml
- ../protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный разработчик. Моя задача — преобразовать `WorkOrder` в полностью реализованный и семантически богатый код на языке Kotlin.</SPECIALIZATION>
<CORE_GOAL>Создать готовый к работе, семантически размеченный и соответствующий всем контрактам код, который реализует поставленную задачу, и передать его на проверку.</CORE_GOAL>
</ROLE_DEFINITION>
<MASTER_WORKFLOW name="Engineer_Workflow">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-developer', TaskType='type::development')"/>
<IF condition="WorkOrder IS NULL">
<TERMINATE/>
</IF>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Implement_And_Test">
<ACTION>Создать ветку для разработки: `feature/{WorkOrder.ID}-{short_title}`.</ACTION>
<ACTION>Выполнить основную работу по реализации, следуя `WorkOrder` и `SEMANTIC_ENRICHMENT_PROTOCOL`.</ACTION>
<ACTION>Запустить локальные тесты и сборку для проверки корректности.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Create_Pull_Request">
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='feat: {WorkOrder.Title}', Body='Closes #{WorkOrder.ID}', HeadBranch=..., BaseBranch='main')"/>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Create_QA_Task">
<LET name="QaTaskID" value="CALL MyTaskChannel.CreateTask(Title='QA: Проверить реализацию {WorkOrder.Title}', Body='PR: #{PrID}\nIssue: #{WorkOrder.ID}', Assignee='agent-qa', Labels='type::quality-assurance,status::pending')"/>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::pending-qa')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Log_Execution_Metrics">
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_ROLE_PROTOCOL>

View File

@@ -1,58 +0,0 @@
<AI_AGENT_ROLE_PROTOCOL name="QA_Tester">
<EXTENDS from="base_role.xml"/>
<META>
<DESCRIPTION>Проверяет соответствие реализации бизнес-требованиям и техническим спецификациям.</DESCRIPTION>
<VERSION>2.0</VERSION>
<METRICS_TO_COLLECT>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="qa_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ../interfaces/task_channel_interface.xml
- ../protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный QA-инженер. Моя задача — анализировать требования, создавать тестовые планы и проверять, что реализация соответствует как бизнес-логике, так и техническим стандартам проекта.</SPECIALIZATION>
<CORE_GOAL>Обеспечить качество продукта путем выявления дефектов, несоответствий и узких мест в реализации.</CORE_GOAL>
</ROLE_DEFINITION>
<MASTER_WORKFLOW name="QA_Workflow">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-qa', TaskType='type::quality-assurance')"/>
<IF condition="WorkOrder IS NULL">
<TERMINATE/>
</IF>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Execute_QA_Audit">
<ACTION>Извлечь `PULL_REQUEST_ID` и `DEVELOPER_ISSUE_ID` из тела `WorkOrder`.</ACTION>
<ACTION>Провести аудит кода и функциональное тестирование на основе `PULL_REQUEST_ID`.</ACTION>
<ACTION>Сгенерировать `DefectReport` если найдены проблемы.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Finalize_Task">
<IF condition="DefectReport IS NULL">
<SUCCESS_PATH>
<ACTION>CALL MyTaskChannel.MergeAndComplete(IssueID={DEVELOPER_ISSUE_ID}, PrID={PULL_REQUEST_ID}, BranchToDelete=...)</ACTION>
</SUCCESS_PATH>
</IF>
<ELSE>
<FAILURE_PATH>
<ACTION>CALL MyTaskChannel.ReturnToDev(IssueID={DEVELOPER_ISSUE_ID}, PrID={PULL_REQUEST_ID}, DefectReport={DefectReport})</ACTION>
</FAILURE_PATH>
</ELSE>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::completed')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Log_Execution_Metrics">
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_ROLE_PROTOCOL>

View File

@@ -1,105 +0,0 @@
<![CDATA[
<AI_AGENT_SEMANTIC_ENRICHMENT_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантического Обогащения'**. Главная задача — обогащение кодовой базы семантической информацией согласно `SEMANTIC_ENRICHMENT_PROTOCOL`.</PURPOSE>
<VERSION>1.0</VERSION>
<METRICS_TO_COLLECT>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="enrichment_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ..agent_promts/interfaces/task_channel_interface.xml
- ..agent_promts/protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я действую как агент семантического обогащения. Моя задача - находить и размечать код, добавляя ему семантическую ценность в соответствии с протоколом.</SPECIALIZATION>
<CORE_GOAL>Проактивно обогащать кодовую базу семантической разметкой для улучшения машиночитаемости и анализа.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Enrich_Dont_Change_Logic">
<DESCRIPTION>Моя работа заключается в добавлении семантических комментариев и аннотаций, не изменяя логику существующего кода.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Traceable_And_Reviewable">
<DESCRIPTION>Все изменения должны быть доступны для просмотра, например, через Pull Request.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Initialization">
<ACTION>Загрузить и полностью проанализировать `agent_promts/protocols/semantic_enrichment_protocol.xml`, включая все вложенные `INCLUDE` файлы, для построения полного набора правил в памяти.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TASK_SPECIFICATION name="Enrichment_Task">
<DESCRIPTION>Задачи для этой роли определяют, какие части кодовой базы нужно обогатить.</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<ENRICHMENT_TASK>
<SCOPE>full_project | directory | file_list</SCOPE>
<TARGET>
<!-- Для directory: path/to/dir -->
<!-- Для file_list: список файлов -->
</TARGET>
</ENRICHMENT_TASK>
]]>
</STRUCTURE>
</TASK_SPECIFICATION>
<MASTER_WORKFLOW name="Enrich_Code_And_Create_PR">
<WORKFLOW_STEP id="1" name="Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-enrichment', TaskType='type::enrichment')"/>
<IF condition="WorkOrder IS NULL">
<TERMINATE/>
</IF>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, NewStatus='status::in-progress')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Execute_Enrichment">
<ACTION>Извлечь `<ENRICHMENT_TASK>` из `WorkOrder`.</ACTION>
<LET name="BranchName">feature/{WorkOrder.ID}/semantic-enrichment</LET>
<ACTION>CALL MyTaskChannel.CreateBranch(BranchName={BranchName})</ACTION>
<ACTION>Определить `files_to_process` на основе `SCOPE` и `TARGET`.</ACTION>
<ACTION>Для каждого файла в `files_to_process` применить правила из `SEMANTIC_ENRICHMENT_PROTOCOL`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Commit_And_PR">
<IF condition="есть_изменения">
<ACTION>Сделать коммит с сообщением: `feat(enrichment): apply semantic markup`.</ACTION>
<ACTION>CALL MyTaskChannel.CommitChanges(...)</ACTION>
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='feat(enrichment): Semantic Markup', Body='Closes #{WorkOrder.ID}', HeadBranch={BranchName}, BaseBranch='main')"/>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Enrichment complete. PR #{PrID} is ready for review.')</ACTION>
</IF>
<ELSE>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Enrichment complete. No new semantic markup was added.')</ACTION>
</ELSE>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Finalize">
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, NewStatus='status::completed')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Log_Metrics">
<ACTION>Отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="6" name="Log_Completion">
<REQUIRES_CHANNEL type="LogSink" as="MyLogSink"/>
<LET name="EnrichmentMetrics" value="CALL MyMetricsSink.GetMetrics(group_id='enrichment_specific')"/>
<LET name="LogMessage">
`WorkOrder {WorkOrder.ID} completed.
- Files Processed: {EnrichmentMetrics.files_processed}
- Entities Enriched: {EnrichmentMetrics.entities_enriched}
- Relations Added: {EnrichmentMetrics.relations_added}
- Contracts Added: {EnrichmentMetrics.contracts_added}
- Logs Added: {EnrichmentMetrics.logs_added}`
</LET>
<ACTION>CALL MyLogSink.Log(FileName="logs/enrichment_agent_log.txt", Content={LogMessage})</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_SEMANTIC_ENRICHMENT_PROTOCOL>
]]>

View File

@@ -1,70 +0,0 @@
<AI_AGENT_SEMANTIC_LINTER_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантической Разметки'**. Главная задача — приведение кодовой базы в полное соответствие с `SEMANTIC_ENRICHMENT_PROTOCOL`.</PURPOSE>
<VERSION>5.0</VERSION>
<METRICS_TO_COLLECT>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="linter_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ..agent_promts/interfaces/task_channel_interface.xml
- ..agent_promts/protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, действую как автоматизированный хранитель чистоты кода. Моя единственная задача — обеспечить, чтобы каждый файл в указанной области соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`.</SPECIALIZATION>
<CORE_GOAL>Поддерживать 100% семантическую чистоту и машиночитаемость кодовой базы, делая все изменения отслеживаемыми через систему контроля версий.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Code_Logic_Is_Immutable">
<DESCRIPTION>Работа касается исключительно метаданных в комментариях, а не исполняемого кода.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Changes_Are_Reviewable">
<DESCRIPTION>Результатом работы всегда является Pull Request или аналогичный артефакт, если это поддерживается каналом задач.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>
<IF condition="WorkOrder IS NULL">
<TERMINATE/>
</IF>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Prepare_And_Execute_Linting">
<ACTION>Извлечь из тела `WorkOrder` блок `<LINTING_TASK>` и определить `MODE` и `TARGET`.</ACTION>
<LET name="BranchName">chore/{WorkOrder.ID}/semantic-linting-{MODE}</LET>
<ACTION>CALL MyTaskChannel.CreateBranch(BranchName={BranchName})</ACTION>
<ACTION>Определить список `files_to_process` в зависимости от `MODE`.</ACTION>
<ACTION>Выполнить обогащение для каждого файла в `files_to_process` и собрать список `modified_files`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Commit_And_Create_PR">
<IF condition="modified_files IS NOT EMPTY">
<ACTION>Сформировать коммит: `chore(lint): apply semantic enrichment\n\nFiles modified: {count}`</ACTION>
<ACTION>CALL MyTaskChannel.CommitChanges(CommitMessage=...)</ACTION>
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='chore(lint): Semantic Enrichment', Body='Closes #{WorkOrder.ID}', HeadBranch={BranchName}, BaseBranch='main')"/>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Linting complete. Pull Request #{PrID} created for review.')</ACTION>
</IF>
<ELSE>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Linting complete. No semantic violations found.')</ACTION>
</ELSE>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Finalize_Task">
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::completed')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Log_Execution_Metrics">
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_SEMANTIC_LINTER_PROTOCOL>

View File

@@ -1,55 +0,0 @@
<!-- File: agent_promts/shared/metrics_catalog.xml -->
<METRICS_CATALOG>
<DESCRIPTION>Централизованный каталог всех LLM-ориентированных метрик для анализа работы агентов.</DESCRIPTION>
<METRIC_GROUP id="core_metrics">
<METRIC id="total_execution_time_ms" type="integer" description="Общее время выполнения задачи от начала до конца."/>
<METRIC id="turn_count" type="integer" description="Количество итераций (сообщений 'вопрос-ответ') для выполнения задачи."/>
<METRIC id="llm_token_usage_per_turn" type="list" description="Статистика по токенам для каждой итерации: {turn, prompt_tokens, completion_tokens}."/>
<METRIC id="tool_calls_log" type="list" description="Полный журнал вызовов инструментов: {turn, tool_name, arguments, result}."/>
<METRIC id="final_outcome" type="string" description="Итоговый результат работы (например, SUCCESS, FAILURE, NO_CHANGES)."/>
</METRIC_GROUP>
<METRIC_GROUP id="coherence_metrics">
<METRIC id="redundant_actions_count" type="integer" description="Счетчик избыточных последовательных действий (например, повторное чтение файла)."/>
<METRIC id="self_correction_count" type="integer" description="Счетчик явных самокоррекций агента (например, 'Я был неправ, попробую другой подход...')."/>
</METRIC_GROUP>
<METRIC_GROUP id="architect_specific">
<METRIC id="plan_revisions_count" type="integer" description="Количество переделок плана после обратной связи от пользователя."/>
<METRIC id="format_adherence_score" type="boolean" description="Соответствие ответа агента требуемому XML-формату."/>
</METRIC_GROUP>
<METRIC_GROUP id="documentation_specific">
<METRIC id="sync_audit_stats" type="object" description="Статистика аудита: {files_scanned, entities_found, relations_found}."/>
<METRIC id="manifest_diff_stats" type="object" description="Изменения в манифесте: {nodes_added, nodes_updated, nodes_removed}."/>
</METRIC_GROUP>
<METRIC_GROUP id="engineer_specific">
<METRIC id="code_generation_stats" type="object" description="Статистика по коду: {files_created, files_modified, lines_of_code_generated}."/>
<METRIC id="semantic_enrichment_stats" type="object" description="Насколько хорошо код был обогащен семантикой: {entities_added, relations_added}."/>
<METRIC id="static_analysis_issues_introduced" type="integer" description="Количество новых проблем, обнаруженных статическим анализатором в сгенерированном коде."/>
<METRIC id="build_breaks_count" type="integer" description="Сколько раз сгенерированный код приводил к ошибке сборки."/>
</METRIC_GROUP>
<METRIC_GROUP id="linter_specific">
<METRIC id="linting_scope" type="object" description="Область проверки: {mode, files_to_process_count}."/>
<METRIC id="linting_results" type="object" description="Результаты работы: {files_modified, violations_fixed}."/>
</METRIC_GROUP>
<METRIC_GROUP id="qa_specific">
<METRIC id="test_plan_coverage" type="float" description="Процент покрытия требований тестовым планом."/>
<METRIC id="defects_found" type="integer" description="Количество найденных дефектов."/>
<METRIC id="automated_tests_run" type="integer" description="Количество запущенных автоматизированных тестов."/>
<METRIC id="manual_verification_time_min" type="integer" description="Время, затраченное на ручную проверку, в минутах."/>
</METRIC_GROUP>
<METRIC_GROUP id="enrichment_specific">
<METRIC name="files_processed" type="integer" unit="files">Количество обработанных файлов.</METRIC>
<METRIC name="entities_enriched" type="integer" unit="entities">Количество обогащенных сущностей (добавлены якоря ENTITY).</METRIC>
<METRIC name="relations_added" type="integer" unit="relations">Количество добавленных семантических связей (якоря RELATION).</METRIC>
<METRIC name="contracts_added" type="integer" unit="contracts">Количество добавленных KDoc-контрактов.</METRIC>
<METRIC name="logs_added" type="integer" unit="logs">Количество добавленных структурированных логов.</METRIC>
</METRIC_GROUP>
</METRICS_CATALOG>

View File

@@ -6,7 +6,6 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
} }
android { android {
@@ -31,24 +30,20 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro", "proguard-rules.pro"
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "1.8"
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true buildConfig = true
aidl = false
renderScript = false
resValues = true
shaders = false
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler kotlinCompilerExtensionVersion = Versions.composeCompiler
@@ -58,10 +53,6 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
lint {
checkReleaseBuilds = false
abortOnError = false
}
} }
dependencies { dependencies {
@@ -69,8 +60,6 @@ dependencies {
implementation(project(":data")) implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity) // [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":ui"))
implementation(project(":feature:inventory"))
// [DEPENDENCY] AndroidX // [DEPENDENCY] AndroidX
implementation(Libs.coreKtx) implementation(Libs.coreKtx)
@@ -87,7 +76,9 @@ dependencies {
implementation(Libs.navigationCompose) implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose) implementation(Libs.hiltNavigationCompose)
// ktlint(project(":data:semantic-ktlint-rules"))
// [DEPENDENCY] DI (Hilt) // [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid) implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler) kapt(Libs.hiltCompiler)

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.homebox.lens">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt // [FILE] MainActivity.kt
// [SEMANTICS] app, ui, activity, entrypoint // [SEMANTICS] ui, activity, entrypoint
package com.homebox.lens package com.homebox.lens
// [IMPORTS] // [IMPORTS]
@@ -21,7 +22,7 @@ import timber.log.Timber
// [ENTITY: Activity('MainActivity')] // [ENTITY: Activity('MainActivity')]
/** /**
* @summary The main and only Activity in the application. * @summary Главная и единственная Activity в приложении.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt // [FILE] MainApplication.kt
// [SEMANTICS] app, hilt, timber, entrypoint // [SEMANTICS] application, hilt, timber
package com.homebox.lens package com.homebox.lens
// [IMPORTS] // [IMPORTS]
@@ -10,7 +11,7 @@ import timber.log.Timber
// [ENTITY: Application('MainApplication')] // [ENTITY: Application('MainApplication')]
/** /**
* @summary The entry point of the application. Initializes Hilt and Timber. * @summary Точка входа в приложение. Инициализирует Hilt и Timber.
*/ */
@HiltAndroidApp @HiltAndroidApp
class MainApplication : Application() { class MainApplication : Application() {

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt // [FILE] NavGraph.kt
// [SEMANTICS] app, ui, navigation // [SEMANTICS] navigation, compose, nav_host
package com.homebox.lens.navigation package com.homebox.lens.navigation
@@ -15,31 +16,25 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.homebox.lens.ui.screen.dashboard.DashboardScreen import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.feature.inventory.ui.InventoryScreen import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.labeledit.LabelEditScreen
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen import com.homebox.lens.ui.screen.setup.SetupScreen
import com.homebox.lens.ui.screen.settings.SettingsScreen
import com.homebox.lens.ui.screen.splash.SplashScreen
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.navigation.Screen
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('NavGraph')] // [ENTITY: Function('NavGraph')]
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')] // [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')] // [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
// [RELATION: Function('NavGraph')] -> [USES] -> [Screen('SplashScreen')]
/** /**
* @summary Defines the navigation graph for the entire application using Jetpack Compose Navigation. * @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
* @param navController The navigation controller. * @param navController Контроллер навигации.
* @see Screen * @see Screen
* @sideeffect Registers all screens and manages the navigation state. * @sideeffect Регистрирует все экраны и управляет состоянием навигации.
* @invariant The start screen is `Screen.Splash`. * @invariant Стартовый экран - `Screen.Setup`.
*/ */
@Composable @Composable
fun NavGraph( fun NavGraph(
@@ -51,19 +46,15 @@ fun NavGraph(
val navigationActions = remember(navController) { val navigationActions = remember(navController) {
NavigationActions(navController) NavigationActions(navController)
} }
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Splash.route startDestination = Screen.Setup.route
) { ) {
composable(route = Screen.Splash.route) {
SplashScreen(navController = navController)
}
composable(route = Screen.Setup.route) { composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = { SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) { navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { popUpTo(Screen.Setup.route) { inclusive = true }
inclusive = true
}
} }
}) })
} }
@@ -73,8 +64,8 @@ fun NavGraph(
navigationActions = navigationActions navigationActions = navigationActions
) )
} }
composable(route = Screen.Inventory.route) { composable(route = Screen.InventoryList.route) {
InventoryScreen( InventoryListScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions
) )
@@ -98,10 +89,7 @@ fun NavGraph(
) )
} }
composable(Screen.LabelsList.route) { composable(Screen.LabelsList.route) {
LabelsListScreen( LabelsListScreen(navController = navController)
currentRoute = currentRoute,
navigationActions = navigationActions
)
} }
composable(route = Screen.LocationsList.route) { composable(route = Screen.LocationsList.route) {
LocationsListScreen( LocationsListScreen(
@@ -109,7 +97,7 @@ fun NavGraph(
navigationActions = navigationActions, navigationActions = navigationActions,
onLocationClick = { locationId -> onLocationClick = { locationId ->
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen // [AI_NOTE]: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.Inventory.route) navController.navigate(Screen.InventoryList.route)
}, },
onAddNewLocationClick = { onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new")) navController.navigate(Screen.LocationEdit.createRoute("new"))
@@ -122,35 +110,12 @@ fun NavGraph(
locationId = locationId locationId = locationId
) )
} }
composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen(
locationId = locationId
)
}
composable(
route = Screen.LabelEdit.route,
arguments = listOf(navArgument("labelId") { nullable = true })
) { backStackEntry ->
val labelId = backStackEntry.arguments?.getString("labelId")
LabelEditScreen(
labelId = labelId,
onBack = { navController.popBackStack() },
onLabelSaved = { navController.popBackStack() }
)
}
composable(route = Screen.Search.route) { composable(route = Screen.Search.route) {
SearchScreen( SearchScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions
) )
} }
composable(route = Screen.Settings.route) {
SettingsScreen(
currentRoute = currentRoute,
navigationActions = navigationActions
)
}
} }
} }
// [END_ENTITY: Function('NavGraph')] // [END_ENTITY: Function('NavGraph')]

View File

@@ -1,26 +1,26 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavigationActions.kt // [FILE] NavigationActions.kt
// [SEMANTICS] ui, navigation, actions // [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation package com.homebox.lens.navigation
// [IMPORTS] // [IMPORTS]
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import timber.log.Timber import timber.log.Timber
import com.homebox.lens.ui.navigation.Screen
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Class('NavigationActions')] // [ENTITY: Class('NavigationActions')]
// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')] // [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
/** /**
* @summary Wrapper class over NavHostController to provide typed navigation actions. * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
* @param navController The Jetpack Navigation controller. * @param navController Контроллер Jetpack Navigation.
* @invariant All navigation actions must use the provided navController. * @invariant Все навигационные действия должны использовать предоставленный navController.
*/ */
class NavigationActions(private val navController: NavHostController) { class NavigationActions(private val navController: NavHostController) {
// [ENTITY: Function('navigateToDashboard')] // [ENTITY: Function('navigateToDashboard')]
/** /**
* @summary Navigation to the main screen. * @summary Навигация на главный экран.
* @sideeffect Clears the back stack up to the main screen to avoid cycles. * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
*/ */
fun navigateToDashboard() { fun navigateToDashboard() {
Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.") Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.")
@@ -49,13 +49,6 @@ class NavigationActions(private val navController: NavHostController) {
} }
// [END_ENTITY: Function('navigateToLabels')] // [END_ENTITY: Function('navigateToLabels')]
// [ENTITY: Function('navigateToLabelEdit')]
fun navigateToLabelEdit(labelId: String? = null) {
Timber.i("[INFO][ACTION][navigate_to_label_edit] Navigating to Label Edit with ID: %s", labelId)
navController.navigate(Screen.LabelEdit.createRoute(labelId))
}
// [END_ENTITY: Function('navigateToLabelEdit')]
// [ENTITY: Function('navigateToSearch')] // [ENTITY: Function('navigateToSearch')]
fun navigateToSearch() { fun navigateToSearch() {
Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.") Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.")
@@ -65,19 +58,10 @@ class NavigationActions(private val navController: NavHostController) {
} }
// [END_ENTITY: Function('navigateToSearch')] // [END_ENTITY: Function('navigateToSearch')]
// [ENTITY: Function('navigateToInventory')]
fun navigateToInventory() {
Timber.i("[INFO][ACTION][navigate_to_inventory] Navigating to Inventory.")
navController.navigate(Screen.Inventory.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToInventory')]
// [ENTITY: Function('navigateToInventoryListWithLabel')] // [ENTITY: Function('navigateToInventoryListWithLabel')]
fun navigateToInventoryListWithLabel(labelId: String) { fun navigateToInventoryListWithLabel(labelId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId) Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId)
val route = Screen.Inventory.withFilter("label", labelId) val route = Screen.InventoryList.withFilter("label", labelId)
navController.navigate(route) navController.navigate(route)
} }
// [END_ENTITY: Function('navigateToInventoryListWithLabel')] // [END_ENTITY: Function('navigateToInventoryListWithLabel')]
@@ -85,7 +69,7 @@ class NavigationActions(private val navController: NavHostController) {
// [ENTITY: Function('navigateToInventoryListWithLocation')] // [ENTITY: Function('navigateToInventoryListWithLocation')]
fun navigateToInventoryListWithLocation(locationId: String) { fun navigateToInventoryListWithLocation(locationId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId) Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId)
val route = Screen.Inventory.withFilter("location", locationId) val route = Screen.InventoryList.withFilter("location", locationId)
navController.navigate(route) navController.navigate(route)
} }
// [END_ENTITY: Function('navigateToInventoryListWithLocation')] // [END_ENTITY: Function('navigateToInventoryListWithLocation')]
@@ -93,17 +77,10 @@ class NavigationActions(private val navController: NavHostController) {
// [ENTITY: Function('navigateToCreateItem')] // [ENTITY: Function('navigateToCreateItem')]
fun navigateToCreateItem() { fun navigateToCreateItem() {
Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.") Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.")
navController.navigate(Screen.ItemEdit.createRoute()) navController.navigate(Screen.ItemEdit.createRoute("new"))
} }
// [END_ENTITY: Function('navigateToCreateItem')] // [END_ENTITY: Function('navigateToCreateItem')]
// [ENTITY: Function('navigateToItemDetails')]
fun navigateToItemDetails(itemId: String) {
Timber.i("[INFO][ACTION][navigate_to_item_details] Navigating to Item Details with ID: %s", itemId)
navController.navigate(Screen.ItemDetails.createRoute(itemId))
}
// [END_ENTITY: Function('navigateToItemDetails')]
// [ENTITY: Function('navigateToLogout')] // [ENTITY: Function('navigateToLogout')]
fun navigateToLogout() { fun navigateToLogout() {
Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.") Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.")

View File

@@ -1,18 +1,15 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] Screen.kt // [FILE] Screen.kt
// [SEMANTICS] ui, navigation, routes // [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.ui.navigation package com.homebox.lens.navigation
// [ENTITY: SealedClass('Screen')] // [ENTITY: SealedClass('Screen')]
/** /**
* @summary Sealed class for defining navigation routes in the application. * @summary Запечатанный класс для определения маршрутов навигации в приложении.
* @description Provides type safety during navigation. * @description Обеспечивает типобезопасность при навигации.
* @param route The string identifier of the route. * @param route Строковый идентификатор маршрута.
*/ */
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
// [ENTITY: Object('Splash')]
data object Splash : Screen("splash_screen")
// [END_ENTITY: Object('Splash')]
// [ENTITY: Object('Setup')] // [ENTITY: Object('Setup')]
data object Setup : Screen("setup_screen") data object Setup : Screen("setup_screen")
// [END_ENTITY: Object('Setup')] // [END_ENTITY: Object('Setup')]
@@ -21,35 +18,35 @@ sealed class Screen(val route: String) {
data object Dashboard : Screen("dashboard_screen") data object Dashboard : Screen("dashboard_screen")
// [END_ENTITY: Object('Dashboard')] // [END_ENTITY: Object('Dashboard')]
// [ENTITY: Object('Inventory')] // [ENTITY: Object('InventoryList')]
data object Inventory : Screen("inventory_screen") { data object InventoryList : Screen("inventory_list_screen") {
// [ENTITY: Function('withFilter')] // [ENTITY: Function('withFilter')]
/** /**
* @summary Creates a route for the inventory screen with a filter parameter. * @summary Создает маршрут для экрана списка инвентаря с параметром фильтра.
* @param key The filter key (e.g., "label" or "location"). * @param key Ключ фильтра (например, "label" или "location").
* @param value The filter value (e.g., the ID of the label or location). * @param value Значение фильтра (например, ID метки или местоположения).
* @return A string of the full route with a query parameter. * @return Строку полного маршрута с query-параметром.
* @throws IllegalArgumentException if the key or value is blank. * @throws IllegalArgumentException если ключ или значение пустые.
*/ */
fun withFilter(key: String, value: String): String { fun withFilter(key: String, value: String): String {
require(key.isNotBlank()) { "Filter key cannot be blank." } require(key.isNotBlank()) { "Filter key cannot be blank." }
require(value.isNotBlank()) { "Filter value cannot be blank." } require(value.isNotBlank()) { "Filter value cannot be blank." }
val constructedRoute = "inventory_screen?$key=$value" val constructedRoute = "inventory_list_screen?$key=$value"
check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." } check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." }
return constructedRoute return constructedRoute
} }
// [END_ENTITY: Function('withFilter')] // [END_ENTITY: Function('withFilter')]
} }
// [END_ENTITY: Object('Inventory')] // [END_ENTITY: Object('InventoryList')]
// [ENTITY: Object('ItemDetails')] // [ENTITY: Object('ItemDetails')]
data object ItemDetails : Screen("item_details_screen/{itemId}") { data object ItemDetails : Screen("item_details_screen/{itemId}") {
// [ENTITY: Function('createRoute')] // [ENTITY: Function('createRoute')]
/** /**
* @summary Creates a route for the item details screen with the specified ID. * @summary Создает маршрут для экрана деталей элемента с указанным ID.
* @param itemId The ID of the item to display. * @param itemId ID элемента для отображения.
* @return A string of the full route. * @return Строку полного маршрута.
* @throws IllegalArgumentException if itemId is blank. * @throws IllegalArgumentException если itemId пустой.
*/ */
fun createRoute(itemId: String): String { fun createRoute(itemId: String): String {
require(itemId.isNotBlank()) { "itemId не может быть пустым." } require(itemId.isNotBlank()) { "itemId не может быть пустым." }
@@ -65,9 +62,9 @@ sealed class Screen(val route: String) {
data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") { data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") {
// [ENTITY: Function('createRoute')] // [ENTITY: Function('createRoute')]
/** /**
* @summary Creates a route for the item edit screen with the specified ID. * @summary Создает маршрут для экрана редактирования элемента с указанным ID.
* @param itemId The ID of the item to edit. Null if a new item is being created. * @param itemId ID элемента для редактирования. Null, если создается новый элемент.
* @return A string of the full route. * @return Строку полного маршрута.
*/ */
fun createRoute(itemId: String? = null): String { fun createRoute(itemId: String? = null): String {
return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen" return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen"
@@ -80,21 +77,6 @@ sealed class Screen(val route: String) {
data object LabelsList : Screen("labels_list_screen") data object LabelsList : Screen("labels_list_screen")
// [END_ENTITY: Object('LabelsList')] // [END_ENTITY: Object('LabelsList')]
// [ENTITY: Object('LabelEdit')]
data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Creates a route for the label edit screen with the specified ID.
* @param labelId The ID of the label to edit. Null if a new label is being created.
* @return A string of the full route.
*/
fun createRoute(labelId: String? = null): String {
return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen"
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: Object('LabelEdit')]
// [ENTITY: Object('LocationsList')] // [ENTITY: Object('LocationsList')]
data object LocationsList : Screen("locations_list_screen") data object LocationsList : Screen("locations_list_screen")
// [END_ENTITY: Object('LocationsList')] // [END_ENTITY: Object('LocationsList')]
@@ -103,10 +85,10 @@ sealed class Screen(val route: String) {
data object LocationEdit : Screen("location_edit_screen/{locationId}") { data object LocationEdit : Screen("location_edit_screen/{locationId}") {
// [ENTITY: Function('createRoute')] // [ENTITY: Function('createRoute')]
/** /**
* @summary Creates a route for the location edit screen with the specified ID. * @summary Создает маршрут для экрана редактирования местоположения с указанным ID.
* @param locationId The ID of the location to edit. * @param locationId ID местоположения для редактирования.
* @return A string of the full route. * @return Строку полного маршрута.
* @throws IllegalArgumentException if locationId is blank. * @throws IllegalArgumentException если locationId пустой.
*/ */
fun createRoute(locationId: String): String { fun createRoute(locationId: String): String {
require(locationId.isNotBlank()) { "locationId не может быть пустым." } require(locationId.isNotBlank()) { "locationId не может быть пустым." }
@@ -121,10 +103,6 @@ sealed class Screen(val route: String) {
// [ENTITY: Object('Search')] // [ENTITY: Object('Search')]
data object Search : Screen("search_screen") data object Search : Screen("search_screen")
// [END_ENTITY: Object('Search')] // [END_ENTITY: Object('Search')]
// [ENTITY: Object('Settings')]
data object Settings : Screen("settings_screen")
// [END_ENTITY: Object('Settings')]
} }
// [END_ENTITY: SealedClass('Screen')] // [END_ENTITY: SealedClass('Screen')]
// [END_FILE_Screen.kt] // [END_FILE_Screen.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.common
// [FILE] AppDrawer.kt // [FILE] AppDrawer.kt
// [SEMANTICS] ui, common, navigation // [SEMANTICS] ui, common, navigation_drawer
package com.homebox.lens.ui.common package com.homebox.lens.ui.common
// [IMPORTS] // [IMPORTS]
@@ -21,18 +22,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.homebox.lens.ui.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.navigation.Screen import com.homebox.lens.navigation.Screen
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('AppDrawerContent')] // [ENTITY: Function('AppDrawerContent')]
// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
/** /**
* @summary Content for the side navigation menu (Drawer). * @summary Контент для бокового навигационного меню (Drawer).
* @param currentRoute The current route to highlight the active item. * @param currentRoute Текущий маршрут для подсветки активного элемента.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
* @param onCloseDrawer Lambda to close the side menu. * @param onCloseDrawer Лямбда для закрытия бокового меню.
*/ */
@Composable @Composable
internal fun AppDrawerContent( internal fun AppDrawerContent(
@@ -65,14 +66,6 @@ internal fun AppDrawerContent(
onCloseDrawer() onCloseDrawer()
} }
) )
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.inventory_list_title)) },
selected = currentRoute == Screen.Inventory.route,
onClick = {
navigationActions.navigateToInventory()
onCloseDrawer()
}
)
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) }, label = { Text(stringResource(id = R.string.nav_locations)) },
selected = currentRoute == Screen.LocationsList.route, selected = currentRoute == Screen.LocationsList.route,

View File

@@ -1,5 +1,7 @@
// [PACKAGE] com.homebox.lens.ui.common
// [FILE] MainScaffold.kt // [FILE] MainScaffold.kt
// [SEMANTICS] ui, common, scaffold // [SEMANTICS] ui, common, scaffold, navigation_drawer
package com.homebox.lens.ui.common package com.homebox.lens.ui.common
// [IMPORTS] // [IMPORTS]
@@ -10,7 +12,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.homebox.lens.ui.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// [END_IMPORTS] // [END_IMPORTS]
@@ -19,14 +21,14 @@ import kotlinx.coroutines.launch
// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')] // [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')]
/** /**
* @summary A common wrapper for screens that includes a Scaffold and Navigation Drawer. * @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
* @param topBarTitle The title for the TopAppBar. * @param topBarTitle Заголовок для TopAppBar.
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
* @param topBarActions A Composable function to display actions (icons) in the TopAppBar. * @param topBarActions Composable-функция для отображения действий (иконок) в TopAppBar.
* @param content The main content of the screen to be displayed inside the Scaffold. * @param content Основное содержимое экрана, которое будет отображено внутри Scaffold.
* @sideeffect Manages the state (open/closed) of the side menu (ModalNavigationDrawer). * @sideeffect Управляет состоянием (открыто/закрыто) бокового меню (ModalNavigationDrawer).
* @invariant The TopAppBar is always displayed with a menu icon. * @invariant TopAppBar всегда отображается с иконкой меню.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -1,75 +0,0 @@
// [FILE] ColorPicker.kt
// [SEMANTICS] app, ui, component, color
package com.homebox.lens.ui.components
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.homebox.lens.R
// [END_IMPORTS]
// [ENTITY: Function('ColorPicker')]
/**
* @summary A component for color selection.
* @param selectedColor The currently selected color in HEX string format (e.g., "#FFFFFF").
* @param onColorSelected A lambda function called when a new color is selected.
* @param modifier A modifier for customizing the appearance.
*/
@Composable
fun ColorPicker(
selectedColor: String,
onColorSelected: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.size(48.dp)
.background(
if (selectedColor.isEmpty()) Color.Transparent else Color(android.graphics.Color.parseColor(selectedColor)),
CircleShape
)
.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
.clickable { /* TODO: Implement a more advanced color selection dialog */ }
)
Spacer(modifier = Modifier.width(16.dp))
OutlinedTextField(
value = selectedColor,
onValueChange = { newValue ->
// Basic validation for hex color
if (newValue.matches(Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"))) {
onColorSelected(newValue)
} else if (newValue.isEmpty() || newValue == "#") {
onColorSelected("#FFFFFF") // Default to white if input is cleared
}
},
label = { Text(stringResource(R.string.label_hex_color)) },
singleLine = true,
modifier = Modifier.weight(1f)
)
}
}
}
// [END_ENTITY: Function('ColorPicker')]
// [END_FILE_ColorPicker.kt]

View File

@@ -1,34 +0,0 @@
// [FILE] LoadingOverlay.kt
// [SEMANTICS] app, ui, component, loading
package com.homebox.lens.ui.components
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
// [END_IMPORTS]
// [ENTITY: Function('LoadingOverlay')]
/**
* @summary A full-screen overlay with a loading indicator.
*/
@Composable
fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// [END_ENTITY: Function('LoadingOverlay')]
// [END_FILE_LoadingOverlay.kt]

View File

@@ -1,62 +0,0 @@
// [FILE] ItemMapper.kt
// [SEMANTICS] app, ui, mapper, item
package com.homebox.lens.ui.mapper
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import javax.inject.Inject
// [ENTITY: Class('ItemMapper')]
/**
* @summary Maps Item data between domain and UI layers.
* @invariant This class is stateless and its methods are pure functions.
*/
class ItemMapper @Inject constructor() {
// [ENTITY: Function('toItem')]
// [RELATION: Function('toItem')] -> [CREATES_INSTANCE_OF] -> [DataClass('Item')]
/**
* @summary Converts a detailed [ItemOut] from the domain layer to a simplified [Item] for the UI layer.
* @param itemOut The [ItemOut] object to convert.
* @return The resulting [Item] object.
* @precondition itemOut MUST NOT be null.
* @postcondition The returned Item will be a valid representation for the UI.
*/
fun toItem(itemOut: ItemOut): Item {
return Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull { it.isPrimary }?.path,
location = itemOut.location?.let { Location(it.id, it.name) },
labels = itemOut.labels.map { Label(it.id, it.name) },
purchasePrice = itemOut.purchasePrice,
createdAt = itemOut.createdAt,
archived = itemOut.isArchived,
assetId = itemOut.assetId,
fields = itemOut.fields.map { com.homebox.lens.domain.model.CustomField(it.name, it.value, it.type) },
insured = itemOut.insured ?: false,
lifetimeWarranty = itemOut.lifetimeWarranty ?: false,
manufacturer = itemOut.manufacturer,
modelNumber = itemOut.modelNumber,
notes = itemOut.notes,
parentId = itemOut.parent?.id,
purchaseFrom = itemOut.purchaseFrom,
purchaseTime = itemOut.purchaseTime,
serialNumber = itemOut.serialNumber,
soldNotes = itemOut.soldNotes,
soldPrice = itemOut.soldPrice,
soldTime = itemOut.soldTime,
soldTo = itemOut.soldTo,
syncChildItemsLocations = itemOut.syncChildItemsLocations ?: false,
warrantyDetails = itemOut.warrantyDetails,
warrantyExpires = itemOut.warrantyExpires
)
}
// [END_ENTITY: Function('toItem')]
}
// [END_ENTITY: Class('ItemMapper')]
// [END_FILE_ItemMapper.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardScreen.kt // [FILE] DashboardScreen.kt
// [SEMANTICS] app, ui, screen, dashboard // [SEMANTICS] ui, screen, dashboard, compose, navigation
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -36,11 +37,11 @@ import timber.log.Timber
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary The main Composable function for the "Dashboard" screen. * @summary Главная Composable-функция для экрана "Панель управления".
* @param viewModel The ViewModel for this screen, provided by Hilt. * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
* @sideeffect Calls navigation lambdas upon UI interaction. * @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
*/ */
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
@@ -81,11 +82,11 @@ fun DashboardScreen(
// [ENTITY: Function('DashboardContent')] // [ENTITY: Function('DashboardContent')]
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')] // [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
/** /**
* @summary Displays the main content of the screen depending on the uiState. * @summary Отображает основной контент экрана в зависимости от uiState.
* @param modifier A modifier for styling. * @param modifier Модификатор для стилизации.
* @param uiState The current UI state of the screen. * @param uiState Текущее состояние UI экрана.
* @param onLocationClick A lambda handler for clicking on a location. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onLabelClick A lambda handler for clicking on a label. * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/ */
@Composable @Composable
private fun DashboardContent( private fun DashboardContent(
@@ -131,8 +132,8 @@ private fun DashboardContent(
// [ENTITY: Function('StatisticsSection')] // [ENTITY: Function('StatisticsSection')]
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] // [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
/** /**
* @summary Section for displaying general statistics. * @summary Секция для отображения общей статистики.
* @param statistics The object with statistical data. * @param statistics Объект со статистическими данными.
*/ */
@Composable @Composable
private fun StatisticsSection(statistics: GroupStatistics) { private fun StatisticsSection(statistics: GroupStatistics) {
@@ -163,9 +164,9 @@ private fun StatisticsSection(statistics: GroupStatistics) {
// [ENTITY: Function('StatisticCard')] // [ENTITY: Function('StatisticCard')]
/** /**
* @summary Card for displaying a single statistical indicator. * @summary Карточка для отображения одного статистического показателя.
* @param title The name of the indicator. * @param title Название показателя.
* @param value The value of the indicator. * @param value Значение показателя.
*/ */
@Composable @Composable
private fun StatisticCard(title: String, value: String) { private fun StatisticCard(title: String, value: String) {
@@ -179,8 +180,8 @@ private fun StatisticCard(title: String, value: String) {
// [ENTITY: Function('RecentlyAddedSection')] // [ENTITY: Function('RecentlyAddedSection')]
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary Section for displaying recently added items. * @summary Секция для отображения недавно добавленных элементов.
* @param items The list of items to display. * @param items Список элементов для отображения.
*/ */
@Composable @Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) { private fun RecentlyAddedSection(items: List<ItemSummary>) {
@@ -212,8 +213,8 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
// [ENTITY: Function('ItemCard')] // [ENTITY: Function('ItemCard')]
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary Card for displaying brief information about an item. * @summary Карточка для отображения краткой информации об элементе.
* @param item The item to display. * @param item Элемент для отображения.
*/ */
@Composable @Composable
private fun ItemCard(item: ItemSummary) { private fun ItemCard(item: ItemSummary) {
@@ -235,9 +236,9 @@ private fun ItemCard(item: ItemSummary) {
// [ENTITY: Function('LocationsSection')] // [ENTITY: Function('LocationsSection')]
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary Section for displaying locations as chips. * @summary Секция для отображения местоположений в виде чипсов.
* @param locations The list of locations. * @param locations Список местоположений.
* @param onLocationClick A lambda handler for clicking on a location. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@@ -264,9 +265,9 @@ private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick:
// [ENTITY: Function('LabelsSection')] // [ENTITY: Function('LabelsSection')]
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
/** /**
* @summary Section for displaying labels as chips. * @summary Секция для отображения меток в виде чипсов.
* @param labels The list of labels. * @param labels Список меток.
* @param onLabelClick A lambda handler for clicking on a label. * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@@ -309,10 +310,10 @@ fun DashboardContentSuccessPreview() {
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
), ),
labels = listOf( labels = listOf(
LabelOut(id="1", name="electronics", description = null, color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="2", name="important", description = null, color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", description = null, color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", description = null, color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
), ),
recentlyAddedItems = emptyList() recentlyAddedItems = emptyList()
) )

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardUiState.kt // [FILE] DashboardUiState.kt
// [SEMANTICS] app, ui, state, dashboard // [SEMANTICS] ui, state, dashboard
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -11,8 +12,8 @@ import com.homebox.lens.domain.model.LocationOutCount
// [ENTITY: SealedInterface('DashboardUiState')] // [ENTITY: SealedInterface('DashboardUiState')]
/** /**
* @summary Defines all possible states for the "Dashboard" screen. * @summary Определяет все возможные состояния для экрана "Дэшборд".
* @invariant At any given time, the screen can only be in one of these states. * @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/ */
sealed interface DashboardUiState { sealed interface DashboardUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
@@ -21,11 +22,11 @@ sealed interface DashboardUiState {
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary The state of a successful data load. * @summary Состояние успешной загрузки данных.
* @param statistics The inventory statistics. * @param statistics Статистика по инвентарю.
* @param locations The list of locations with counters. * @param locations Список локаций со счетчиками.
* @param labels The list of all labels. * @param labels Список всех меток.
* @param recentlyAddedItems The list of recently added items. * @param recentlyAddedItems Список недавно добавленных товаров.
*/ */
data class Success( data class Success(
val statistics: GroupStatistics, val statistics: GroupStatistics,
@@ -37,15 +38,15 @@ sealed interface DashboardUiState {
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary The state of an error during data loading. * @summary Состояние ошибки во время загрузки данных.
* @param message A human-readable error message. * @param message Человекочитаемое сообщение об ошибке.
*/ */
data class Error(val message: String) : DashboardUiState data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary The state when data for the screen is being loaded. * @summary Состояние, когда данные для экрана загружаются.
*/ */
data object Loading : DashboardUiState data object Loading : DashboardUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardViewModel.kt // [FILE] DashboardViewModel.kt
// [SEMANTICS] app, ui, viewmodel, dashboard // [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -23,10 +24,10 @@ import javax.inject.Inject
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')] // [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')] // [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
/** /**
* @summary ViewModel for the main screen (Dashboard). * @summary ViewModel для главного экрана (Dashboard).
* @description Orchestrates the loading of data for the Dashboard, using a strict state model * @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
* (`DashboardUiState`), and handles parallel requests without race conditions. * (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
* @invariant `uiState` is always one of the states defined in `DashboardUiState`. * @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/ */
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel @Inject constructor(
@@ -45,10 +46,10 @@ class DashboardViewModel @Inject constructor(
// [ENTITY: Function('loadDashboardData')] // [ENTITY: Function('loadDashboardData')]
/** /**
* @summary Loads all necessary data for the Dashboard screen. * @summary Загружает все необходимые данные для экрана Dashboard.
* @description Executes UseCases in parallel and updates the UI by switching it * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
* between the `Loading`, `Success`, and `Error` states from `DashboardUiState`. * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
* @sideeffect Asynchronously updates `_uiState` with one of the `DashboardUiState` states. * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/ */
fun loadDashboardData() { fun loadDashboardData() {
viewModelScope.launch { viewModelScope.launch {

View File

@@ -0,0 +1,39 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
// [SEMANTICS] ui, screen, inventory, list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('InventoryListScreen')]
// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Список инвентаря".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
*/
@Composable
fun InventoryListScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.inventory_list_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
// [AI_NOTE]: Implement Inventory List Screen UI
Text(text = "Inventory List Screen")
}
}
// [END_ENTITY: Function('InventoryListScreen')]
// [END_FILE_InventoryListScreen.kt]

View File

@@ -0,0 +1,21 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
// [SEMANTICS] ui, viewmodel, inventory_list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('InventoryListViewModel')]
/**
* @summary ViewModel for the inventory list screen.
*/
@HiltViewModel
class InventoryListViewModel @Inject constructor() : ViewModel() {
// [AI_NOTE]: Implement UI state
}
// [END_ENTITY: ViewModel('InventoryListViewModel')]
// [END_FILE_InventoryListViewModel.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt // [FILE] ItemDetailsScreen.kt
// [SEMANTICS] app, ui, screen, details // [SEMANTICS] ui, screen, item, details
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
@@ -16,9 +17,9 @@ import com.homebox.lens.ui.common.MainScaffold
// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable function for the "Item Details" screen. * @summary Composable-функция для экрана "Детали элемента".
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
*/ */
@Composable @Composable
fun ItemDetailsScreen( fun ItemDetailsScreen(

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsViewModel.kt // [FILE] ItemDetailsViewModel.kt
// [SEMANTICS] app, ui, viewmodel, details // [SEMANTICS] ui, viewmodel, item_details
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS] // [IMPORTS]

View File

@@ -1,90 +1,62 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt // [FILE] ItemEditScreen.kt
// [SEMANTICS] app, ui, screen, edit // [SEMANTICS] ui, screen, item, edit
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Composable('ItemEditScreen')] // [ENTITY: Function('ItemEditScreen')]
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')] // [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
// [RELATION: Composable('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')] // [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
// [RELATION: Composable('ItemEditScreen')] -> [CALLS] -> [Composable('MainScaffold')] // [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable function for the "Edit Item" screen. * @summary Composable-функция для экрана "Редактирование элемента".
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
* @param itemId The ID of the item to edit. Null if a new item is being created. * @param itemId ID элемента для редактирования. Null, если создается новый элемент.
* @param viewModel The ViewModel for managing the screen's state. * @param viewModel ViewModel для управления состоянием экрана.
* @param onSaveSuccess A callback invoked after the item is successfully saved. * @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ItemEditScreen( fun ItemEditScreen(
currentRoute: String?, currentRoute: String?,
navigationActions: NavigationActions, navigationActions: NavigationActions,
itemId: String?, itemId: String?,
viewModel: ItemEditViewModel = hiltViewModel(), viewModel: ItemEditViewModel = viewModel(),
onSaveSuccess: () -> Unit onSaveSuccess: () -> Unit
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@@ -113,7 +85,7 @@ fun ItemEditScreen(
topBarTitle = stringResource(id = R.string.item_edit_title), topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions
) { paddingValues -> ) {
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = { floatingActionButton = {
@@ -128,25 +100,13 @@ fun ItemEditScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(it)
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState())
) { ) {
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.fillMaxWidth()) CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
} else { } else {
uiState.item?.let { item -> uiState.item?.let { item ->
// [AI_NOTE]: General Information section for basic item details.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_general_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField( OutlinedTextField(
value = item.name, value = item.name,
onValueChange = { viewModel.updateName(it) }, onValueChange = { viewModel.updateName(it) },
@@ -168,439 +128,12 @@ fun ItemEditScreen(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) // Add more fields as needed
// Location Dropdown
var locationExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = locationExpanded,
onExpandedChange = { locationExpanded = !locationExpanded }
) {
OutlinedTextField(
value = item.location?.name ?: "",
onValueChange = { },
label = { Text(stringResource(R.string.item_edit_location)) },
readOnly = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = locationExpanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = locationExpanded,
onDismissRequest = { locationExpanded = false }
) {
uiState.allLocations.forEach { location ->
DropdownMenuItem(
text = { Text(location.name) },
onClick = {
viewModel.updateLocation(location)
locationExpanded = false
}
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// Labels Dialog
var showLabelsDialog by remember { mutableStateOf(false) }
OutlinedTextField(
value = item.labels.joinToString { it.name },
onValueChange = { },
label = { Text(stringResource(R.string.item_edit_labels)) },
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.clickable { showLabelsDialog = true },
trailingIcon = {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_select_labels))
}
)
if (showLabelsDialog) {
// This state will hold the temporary selections within the dialog
val tempSelectedLabels = remember { mutableStateOf(item.labels.toSet()) }
AlertDialog(
onDismissRequest = { showLabelsDialog = false },
title = { Text(stringResource(R.string.item_edit_select_labels)) },
text = {
Column {
uiState.allLabels.forEach { label ->
val isChecked = tempSelectedLabels.value.contains(label)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
val currentSelection = tempSelectedLabels.value.toMutableSet()
if (isChecked) {
currentSelection.remove(label)
} else {
currentSelection.add(label)
}
tempSelectedLabels.value = currentSelection
}
.padding(vertical = 8.dp)
) {
Checkbox(
checked = isChecked,
onCheckedChange = {
val currentSelection = tempSelectedLabels.value.toMutableSet()
if (it) {
currentSelection.add(label)
} else {
currentSelection.remove(label)
}
tempSelectedLabels.value = currentSelection
}
)
Text(
text = label.name,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
},
confirmButton = {
TextButton(
onClick = {
// Update the ViewModel with the final selection
viewModel.updateLabels(tempSelectedLabels.value.toList())
showLabelsDialog = false
}
) {
Text(stringResource(R.string.dialog_ok))
}
},
dismissButton = {
TextButton(onClick = { showLabelsDialog = false }) {
Text(stringResource(R.string.dialog_cancel))
}
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Purchase Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_purchase_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.purchasePrice?.toString() ?: "",
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_purchase_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.purchaseFrom ?: "",
onValueChange = { viewModel.updatePurchaseFrom(it) },
label = { Text(stringResource(R.string.item_edit_purchase_from)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for purchase time.
var showPurchaseDatePicker by remember { mutableStateOf(false) }
val purchaseDateState = rememberDatePickerState()
OutlinedTextField(
value = item.purchaseTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_purchase_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showPurchaseDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showPurchaseDatePicker = true }
)
if (showPurchaseDatePicker) {
DatePickerDialog(
onDismissRequest = { showPurchaseDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = purchaseDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updatePurchaseTime(selectedDate)
}
showPurchaseDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showPurchaseDatePicker = false })
}
) {
DatePicker(state = purchaseDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Warranty Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_warranty_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_lifetime_warranty))
Switch(
checked = item.lifetimeWarranty,
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.warrantyDetails ?: "",
onValueChange = { viewModel.updateWarrantyDetails(it) },
label = { Text(stringResource(R.string.item_edit_warranty_details)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for warranty expiration.
var showWarrantyDatePicker by remember { mutableStateOf(false) }
val warrantyDateState = rememberDatePickerState()
OutlinedTextField(
value = item.warrantyExpires ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_warranty_expires)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showWarrantyDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showWarrantyDatePicker = true }
)
if (showWarrantyDatePicker) {
DatePickerDialog(
onDismissRequest = { showWarrantyDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = warrantyDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateWarrantyExpires(selectedDate)
}
showWarrantyDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showWarrantyDatePicker = false })
}
) {
DatePicker(state = warrantyDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Identification section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_identification),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.assetId ?: "",
onValueChange = { viewModel.updateAssetId(it) },
label = { Text(stringResource(R.string.item_edit_asset_id)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.serialNumber ?: "",
onValueChange = { viewModel.updateSerialNumber(it) },
label = { Text(stringResource(R.string.item_edit_serial_number)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.manufacturer ?: "",
onValueChange = { viewModel.updateManufacturer(it) },
label = { Text(stringResource(R.string.item_edit_manufacturer)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.modelNumber ?: "",
onValueChange = { viewModel.updateModelNumber(it) },
label = { Text(stringResource(R.string.item_edit_model_number)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Status & Notes section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_status_notes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_archived))
Switch(
checked = item.archived,
onCheckedChange = { viewModel.updateArchived(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_insured))
Switch(
checked = item.insured,
onCheckedChange = { viewModel.updateInsured(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.notes ?: "",
onValueChange = { viewModel.updateNotes(it) },
label = { Text(stringResource(R.string.item_edit_notes)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Sold Information section (conditionally displayed).
if (item.soldTime != null || item.soldPrice != null || item.soldTo != null || item.soldNotes != null) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_sold_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.soldPrice?.toString() ?: "",
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_sold_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldTo ?: "",
onValueChange = { viewModel.updateSoldTo(it) },
label = { Text(stringResource(R.string.item_edit_sold_to)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldNotes ?: "",
onValueChange = { viewModel.updateSoldNotes(it) },
label = { Text(stringResource(R.string.item_edit_sold_notes)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for sold time.
var showSoldDatePicker by remember { mutableStateOf(false) }
val soldDateState = rememberDatePickerState()
OutlinedTextField(
value = item.soldTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_sold_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showSoldDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showSoldDatePicker = true }
)
if (showSoldDatePicker) {
DatePickerDialog(
onDismissRequest = { showSoldDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = soldDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateSoldTime(selectedDate)
}
showSoldDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
}
) {
DatePicker(state = soldDateState)
} }
} }
} }
} }
} }
} }
}} // [END_ENTITY: Function('ItemEditScreen')]
}
}
}
// [END_ENTITY: Composable('ItemEditScreen')]
// [END_FILE_ItemEditScreen.kt] // [END_FILE_ItemEditScreen.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModel.kt // [FILE] ItemEditViewModel.kt
// [SEMANTICS] app, ui, viewmodel, edit // [SEMANTICS] ui, viewmodel, item_edit
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
@@ -8,16 +9,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Item import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemUpdate
import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.usecase.CreateItemUseCase import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.data.mapper.toDomain
import com.homebox.lens.domain.usecase.UpdateItemUseCase import com.homebox.lens.domain.usecase.UpdateItemUseCase
import com.homebox.lens.ui.mapper.ItemMapper
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,15 +32,11 @@ import javax.inject.Inject
* @param item The item being edited, or null if creating a new item. * @param item The item being edited, or null if creating a new item.
* @param isLoading Whether data is currently being loaded or saved. * @param isLoading Whether data is currently being loaded or saved.
* @param error An error message if an operation failed. * @param error An error message if an operation failed.
* @param allLocations A list of all available locations.
* @param allLabels A list of all available labels.
*/ */
data class ItemEditUiState( data class ItemEditUiState(
val item: Item? = null, val item: Item? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null
val allLocations: List<Location> = emptyList(),
val allLabels: List<Label> = emptyList()
) )
// [END_ENTITY: DataClass('ItemEditUiState')] // [END_ENTITY: DataClass('ItemEditUiState')]
@@ -52,25 +44,15 @@ data class ItemEditUiState(
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [Class('ItemMapper')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')] // [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
/** /**
* @summary ViewModel for the item edit screen. * @summary ViewModel for the item edit screen.
* @param createItemUseCase Use case for creating a new item.
* @param updateItemUseCase Use case for updating an existing item.
* @param getItemDetailsUseCase Use case for fetching item details.
* @param getAllLocationsUseCase Use case for fetching all locations.
* @param getAllLabelsUseCase Use case for fetching all labels.
* @param itemMapper Mapper for converting between domain and UI item models.
*/ */
@HiltViewModel @HiltViewModel
class ItemEditViewModel @Inject constructor( class ItemEditViewModel @Inject constructor(
private val createItemUseCase: CreateItemUseCase, private val createItemUseCase: CreateItemUseCase,
private val updateItemUseCase: UpdateItemUseCase, private val updateItemUseCase: UpdateItemUseCase,
private val getItemDetailsUseCase: GetItemDetailsUseCase, private val getItemDetailsUseCase: GetItemDetailsUseCase
private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val itemMapper: ItemMapper
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(ItemEditUiState()) private val _uiState = MutableStateFlow(ItemEditUiState())
@@ -91,93 +73,34 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true, error = null) _uiState.value = _uiState.value.copy(isLoading = true, error = null)
if (itemId == null) { if (itemId == null) {
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.") Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(isLoading = false, item = Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null))
isLoading = false,
item = Item(
id = "",
name = "",
description = null,
quantity = 1,
image = null,
location = null,
labels = emptyList(),
purchasePrice = null,
createdAt = null,
archived = false,
assetId = null,
fields = emptyList(),
insured = false,
lifetimeWarranty = false,
manufacturer = null,
modelNumber = null,
notes = null,
parentId = null,
purchaseFrom = null,
purchaseTime = null,
serialNumber = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = false,
warrantyDetails = null,
warrantyExpires = null
)
)
} else { } else {
try { try {
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId) Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
val itemOut = getItemDetailsUseCase(itemId) val itemOut = getItemDetailsUseCase(itemId)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.") Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val item = itemMapper.toItem(itemOut) val item = Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull()?.path, // Assuming first image is the main one
location = itemOut.location?.let { Location(it.id, it.name) }, // Simplified mapping
labels = itemOut.labels.map { Label(it.id, it.name) }, // Simplified mapping
value = itemOut.value?.toBigDecimal(),
createdAt = itemOut.createdAt
)
_uiState.value = _uiState.value.copy(isLoading = false, item = item) _uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched and mapped item details for ID: %s", itemId) Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId) Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage) _uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
} }
} }
// Load all locations and labels
try {
Timber.i("[INFO][ACTION][fetching_all_locations] Fetching all locations.")
val allLocations = getAllLocationsUseCase().map { Location(it.id, it.name) }
Timber.i("[INFO][ACTION][fetching_all_labels] Fetching all labels.")
val allLabels = getAllLabelsUseCase().map { it.toDomain() }
_uiState.value = _uiState.value.copy(allLocations = allLocations, allLabels = allLabels)
Timber.i("[INFO][ACTION][all_locations_labels_fetched] Successfully fetched all locations and labels.")
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][locations_labels_load_failed] Failed to load locations or labels.")
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
}
} }
} }
// [END_ENTITY: Function('loadItem')] // [END_ENTITY: Function('loadItem')]
// [ENTITY: Function('updateLocation')]
/**
* @summary Updates the location of the item in the UI state.
* @param location The new location for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLocation(location: Location) {
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", location.name)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = location))
}
// [END_ENTITY: Function('updateLocation')]
// [ENTITY: Function('updateLabels')]
/**
* @summary Updates the labels of the item in the UI state.
* @param labels The new list of labels for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLabels(labels: List<Label>) {
Timber.d("[DEBUG][ACTION][updating_item_labels] Updating item labels to: %s", labels.map { it.name }.joinToString())
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = labels))
}
// [END_ENTITY: Function('updateLabels')]
// [ENTITY: Function('saveItem')] // [ENTITY: Function('saveItem')]
/** /**
* @summary Saves the current item, either creating a new one or updating an existing one. * @summary Saves the current item, either creating a new one or updating an existing one.
@@ -194,48 +117,53 @@ class ItemEditViewModel @Inject constructor(
try { try {
if (currentItem.id.isBlank()) { if (currentItem.id.isBlank()) {
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name) Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
val createdItemSummary = createItemUseCase( val createdItemSummary = createItemUseCase(ItemCreate(
ItemCreate(
name = currentItem.name, name = currentItem.name,
description = currentItem.description, description = currentItem.description,
quantity = currentItem.quantity, quantity = currentItem.quantity,
archived = currentItem.archived, assetId = null,
assetId = currentItem.assetId, notes = null,
insured = currentItem.insured, serialNumber = null,
lifetimeWarranty = currentItem.lifetimeWarranty, value = null,
manufacturer = currentItem.manufacturer, purchasePrice = null,
modelNumber = currentItem.modelNumber, purchaseDate = null,
notes = currentItem.notes, warrantyUntil = null,
parentId = currentItem.parentId,
purchaseFrom = currentItem.purchaseFrom,
purchasePrice = currentItem.purchasePrice,
purchaseTime = currentItem.purchaseTime,
serialNumber = currentItem.serialNumber,
soldNotes = currentItem.soldNotes,
soldPrice = currentItem.soldPrice,
soldTime = currentItem.soldTime,
soldTo = currentItem.soldTo,
syncChildItemsLocations = currentItem.syncChildItemsLocations,
warrantyDetails = currentItem.warrantyDetails,
warrantyExpires = currentItem.warrantyExpires,
locationId = currentItem.location?.id, locationId = currentItem.location?.id,
parentId = null,
labelIds = currentItem.labels.map { it.id } labelIds = currentItem.labels.map { it.id }
))
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
val createdItem = Item(
id = createdItemSummary.id,
name = createdItemSummary.name,
description = null, // ItemSummary does not have description
quantity = 0, // ItemSummary does not have quantity
image = null, // ItemSummary does not have image
location = null, // ItemSummary does not have location
labels = emptyList(), // ItemSummary does not have labels
value = null, // ItemSummary does not have value
createdAt = null // ItemSummary does not have createdAt
) )
) _uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
Timber.i("[INFO][ACTION][fetching_full_item_after_creation] Fetching full item details after creation for ID: %s", createdItemSummary.id) Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id)
val createdItemOut = getItemDetailsUseCase(createdItemSummary.id)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping created ItemOut to Item for UI state.")
val item = itemMapper.toItem(createdItemOut)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][new_item_created] Successfully created and mapped new item with ID: %s", createdItemOut.id)
_saveCompleted.emit(Unit) _saveCompleted.emit(Unit)
} else { } else {
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id) Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
val updatedItemOut = updateItemUseCase(currentItem) val updatedItemOut = updateItemUseCase(currentItem)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping updated ItemOut to Item for UI state.") Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val item = itemMapper.toItem(updatedItemOut) val updatedItem = Item(
_uiState.value = _uiState.value.copy(isLoading = false, item = item) id = updatedItemOut.id,
Timber.i("[INFO][ACTION][item_updated] Successfully updated and mapped item with ID: %s", updatedItemOut.id) name = updatedItemOut.name,
description = updatedItemOut.description,
quantity = updatedItemOut.quantity,
image = updatedItemOut.images.firstOrNull()?.path,
location = updatedItemOut.location?.let { Location(it.id, it.name) },
labels = updatedItemOut.labels.map { Label(it.id, it.name) },
value = updatedItemOut.value.toBigDecimal(),
createdAt = updatedItemOut.createdAt
)
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem)
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id)
_saveCompleted.emit(Unit) _saveCompleted.emit(Unit)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -281,234 +209,6 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity)) _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
} }
// [END_ENTITY: Function('updateQuantity')] // [END_ENTITY: Function('updateQuantity')]
// [ENTITY: Function('updateArchived')]
/**
* @summary Updates the archived status of the item in the UI state.
* @param newArchived The new archived status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateArchived(newArchived: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_archived] Updating item archived status to: %s", newArchived)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(archived = newArchived))
}
// [END_ENTITY: Function('updateArchived')]
// [ENTITY: Function('updateAssetId')]
/**
* @summary Updates the asset ID of the item in the UI state.
* @param newAssetId The new asset ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateAssetId(newAssetId: String) {
Timber.d("[DEBUG][ACTION][updating_item_assetId] Updating item asset ID to: %s", newAssetId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId))
}
// [END_ENTITY: Function('updateAssetId')]
// [ENTITY: Function('updateInsured')]
/**
* @summary Updates the insured status of the item in the UI state.
* @param newInsured The new insured status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateInsured(newInsured: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_insured] Updating item insured status to: %s", newInsured)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(insured = newInsured))
}
// [END_ENTITY: Function('updateInsured')]
// [ENTITY: Function('updateLifetimeWarranty')]
/**
* @summary Updates the lifetime warranty status of the item in the UI state.
* @param newLifetimeWarranty The new lifetime warranty status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLifetimeWarranty(newLifetimeWarranty: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_lifetime_warranty] Updating item lifetime warranty status to: %s", newLifetimeWarranty)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(lifetimeWarranty = newLifetimeWarranty))
}
// [END_ENTITY: Function('updateLifetimeWarranty')]
// [ENTITY: Function('updateManufacturer')]
/**
* @summary Updates the manufacturer of the item in the UI state.
* @param newManufacturer The new manufacturer for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateManufacturer(newManufacturer: String) {
Timber.d("[DEBUG][ACTION][updating_item_manufacturer] Updating item manufacturer to: %s", newManufacturer)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(manufacturer = newManufacturer))
}
// [END_ENTITY: Function('updateManufacturer')]
// [ENTITY: Function('updateModelNumber')]
/**
* @summary Updates the model number of the item in the UI state.
* @param newModelNumber The new model number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateModelNumber(newModelNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_model_number] Updating item model number to: %s", newModelNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber))
}
// [END_ENTITY: Function('updateModelNumber')]
// [ENTITY: Function('updateNotes')]
/**
* @summary Updates the notes of the item in the UI state.
* @param newNotes The new notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateNotes(newNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_notes] Updating item notes to: %s", newNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(notes = newNotes))
}
// [END_ENTITY: Function('updateNotes')]
// [ENTITY: Function('updateParentId')]
/**
* @summary Updates the parent ID of the item in the UI state.
* @param newParentId The new parent ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateParentId(newParentId: String) {
Timber.d("[DEBUG][ACTION][updating_item_parent_id] Updating item parent ID to: %s", newParentId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId))
}
// [END_ENTITY: Function('updateParentId')]
// [ENTITY: Function('updatePurchaseFrom')]
/**
* @summary Updates the purchase source of the item in the UI state.
* @param newPurchaseFrom The new purchase source for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseFrom(newPurchaseFrom: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_from] Updating item purchase from to: %s", newPurchaseFrom)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom))
}
// [END_ENTITY: Function('updatePurchaseFrom')]
// [ENTITY: Function('updatePurchasePrice')]
/**
* @summary Updates the purchase price of the item in the UI state.
* @param newPurchasePrice The new purchase price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchasePrice(newPurchasePrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_price] Updating item purchase price to: %s", newPurchasePrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice))
}
// [END_ENTITY: Function('updatePurchasePrice')]
// [ENTITY: Function('updatePurchaseTime')]
/**
* @summary Updates the purchase time of the item in the UI state.
* @param newPurchaseTime The new purchase time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseTime(newPurchaseTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_time] Updating item purchase time to: %s", newPurchaseTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseTime = newPurchaseTime))
}
// [END_ENTITY: Function('updatePurchaseTime')]
// [ENTITY: Function('updateSerialNumber')]
/**
* @summary Updates the serial number of the item in the UI state.
* @param newSerialNumber The new serial number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSerialNumber(newSerialNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_serial_number] Updating item serial number to: %s", newSerialNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber))
}
// [END_ENTITY: Function('updateSerialNumber')]
// [ENTITY: Function('updateSoldNotes')]
/**
* @summary Updates the sold notes of the item in the UI state.
* @param newSoldNotes The new sold notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldNotes(newSoldNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_notes] Updating item sold notes to: %s", newSoldNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldNotes = newSoldNotes))
}
// [END_ENTITY: Function('updateSoldNotes')]
// [ENTITY: Function('updateSoldPrice')]
/**
* @summary Updates the sold price of the item in the UI state.
* @param newSoldPrice The new sold price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldPrice(newSoldPrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_sold_price] Updating item sold price to: %s", newSoldPrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldPrice = newSoldPrice))
}
// [END_ENTITY: Function('updateSoldPrice')]
// [ENTITY: Function('updateSoldTime')]
/**
* @summary Updates the sold time of the item in the UI state.
* @param newSoldTime The new sold time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTime(newSoldTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_time] Updating item sold time to: %s", newSoldTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTime = newSoldTime))
}
// [END_ENTITY: Function('updateSoldTime')]
// [ENTITY: Function('updateSoldTo')]
/**
* @summary Updates the sold to field of the item in the UI state.
* @param newSoldTo The new sold to for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTo(newSoldTo: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_to] Updating item sold to to: %s", newSoldTo)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTo = newSoldTo))
}
// [END_ENTITY: Function('updateSoldTo')]
// [ENTITY: Function('updateSyncChildItemsLocations')]
/**
* @summary Updates the sync child items locations status of the item in the UI state.
* @param newSyncChildItemsLocations The new sync child items locations status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSyncChildItemsLocations(newSyncChildItemsLocations: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_sync_child_items_locations] Updating item sync child items locations status to: %s", newSyncChildItemsLocations)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(syncChildItemsLocations = newSyncChildItemsLocations))
}
// [END_ENTITY: Function('updateSyncChildItemsLocations')]
// [ENTITY: Function('updateWarrantyDetails')]
/**
* @summary Updates the warranty details of the item in the UI state.
* @param newWarrantyDetails The new warranty details for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyDetails(newWarrantyDetails: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_details] Updating item warranty details to: %s", newWarrantyDetails)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails))
}
// [END_ENTITY: Function('updateWarrantyDetails')]
// [ENTITY: Function('updateWarrantyExpires')]
/**
* @summary Updates the warranty expires date of the item in the UI state.
* @param newWarrantyExpires The new warranty expires date for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyExpires(newWarrantyExpires: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_expires] Updating item warranty expires date to: %s", newWarrantyExpires)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyExpires = newWarrantyExpires))
}
// [END_ENTITY: Function('updateWarrantyExpires')]
} }
// [END_ENTITY: ViewModel('ItemEditViewModel')] // [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt] // [END_FILE_ItemEditViewModel.kt]

View File

@@ -1,119 +0,0 @@
// [FILE] LabelEditScreen.kt
// [SEMANTICS] app, ui, screen, edit, label
package com.homebox.lens.ui.screen.labeledit
// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.ui.components.ColorPicker
import com.homebox.lens.ui.components.LoadingOverlay
// [END_IMPORTS]
// [ENTITY: Function('LabelEditScreen')]
// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')]
/**
* @summary Composable function for the "Edit Label" screen.
* @param labelId The ID of the label to edit, or null to create a new one.
* @param onBack Navigation back.
* @param onLabelSaved Action after the label is saved.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelEditScreen(
labelId: String?,
onBack: () -> Unit,
onLabelSaved: () -> Unit,
viewModel: LabelEditViewModel = hiltViewModel()
) {
val uiState = viewModel.uiState
val snackbarHostState = SnackbarHostState()
LaunchedEffect(uiState.isSaved) {
if (uiState.isSaved) {
onLabelSaved()
}
}
LaunchedEffect(uiState.error) {
uiState.error?.let {
snackbarHostState.showSnackbar(
message = it,
actionLabel = "Dismiss",
duration = SnackbarDuration.Short
)
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = {
Text(
text = if (labelId == null) {
stringResource(id = R.string.label_edit_title_create)
} else {
stringResource(id = R.string.label_edit_title_edit)
}
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
}
},
actions = {
IconButton(onClick = viewModel::saveLabel) {
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save))
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
OutlinedTextField(
value = uiState.name,
onValueChange = viewModel::onNameChange,
label = { Text(stringResource(R.string.label_name)) },
isError = uiState.nameError != null,
supportingText = { uiState.nameError?.let { Text(it) } },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = uiState.description.orEmpty(),
onValueChange = viewModel::onDescriptionChange,
label = { Text(stringResource(R.string.label_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
ColorPicker(
selectedColor = uiState.color,
onColorSelected = viewModel::onColorChange,
modifier = Modifier.fillMaxWidth()
)
}
if (uiState.isLoading) {
LoadingOverlay()
}
}
}
// [END_ENTITY: Function('LabelEditScreen')]
// [END_FILE_LabelEditScreen.kt]

View File

@@ -1,125 +0,0 @@
// [FILE] LabelEditViewModel.kt
// [SEMANTICS] app, ui, viewmodel, edit, label
package com.homebox.lens.ui.screen.labeledit
// [IMPORTS]
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.LabelCreate
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LabelUpdate
import com.homebox.lens.domain.usecase.CreateLabelUseCase
import com.homebox.lens.domain.usecase.GetLabelDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateLabelUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('LabelEditViewModel')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetLabelDetailsUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateLabelUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateLabelUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [EMITS_STATE] -> [DataClass('LabelEditUiState')]
@HiltViewModel
class LabelEditViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getLabelDetailsUseCase: GetLabelDetailsUseCase,
private val createLabelUseCase: CreateLabelUseCase,
private val updateLabelUseCase: UpdateLabelUseCase
) : ViewModel() {
var uiState by mutableStateOf(LabelEditUiState())
private set
private val labelId: String? = savedStateHandle["labelId"]
init {
if (labelId != null) {
loadLabelDetails(labelId)
}
}
fun onNameChange(newName: String) {
uiState = uiState.copy(name = newName, nameError = null)
}
fun onDescriptionChange(newDescription: String) {
uiState = uiState.copy(description = newDescription)
}
fun onColorChange(newColor: String) {
uiState = uiState.copy(color = newColor)
}
fun saveLabel() {
viewModelScope.launch {
if (uiState.name.isBlank()) {
uiState = uiState.copy(nameError = "Label name cannot be empty.")
return@launch
}
uiState = uiState.copy(isLoading = true, error = null)
try {
val result = if (labelId == null) {
// [LOG_EVENT] [EVENT_TYPE: LabelCreationAttempt] [DATA: { "labelName": "${uiState.name}" }]
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = uiState.description)
createLabelUseCase(newLabel)
} else {
// [LOG_EVENT] [EVENT_TYPE: LabelUpdateAttempt] [DATA: { "labelId": "$labelId", "labelName": "${uiState.name}" }]
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = uiState.description)
updateLabelUseCase(labelId, updatedLabel)
}
// [LOG_EVENT] [EVENT_TYPE: LabelSaveSuccess] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
uiState = uiState.copy(isSaved = true)
} catch (e: Exception) {
// [LOG_EVENT] [EVENT_TYPE: LabelSaveFailure] [ERROR: "${e.message}"] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
uiState = uiState.copy(error = e.message, isLoading = false)
} finally {
uiState = uiState.copy(isLoading = false)
}
}
}
private fun loadLabelDetails(id: String) {
viewModelScope.launch {
uiState = uiState.copy(isLoading = true, error = null)
try {
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchAttempt] [DATA: { "labelId": "$id" }]
val label = getLabelDetailsUseCase(id)
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchSuccess] [DATA: { "labelId": "$id", "labelName": "${label.name}" }]
uiState = uiState.copy(
name = label.name,
color = label.color,
description = label.description,
isLoading = false,
originalLabel = label
)
} catch (e: Exception) {
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchFailure] [ERROR: "${e.message}"] [DATA: { "labelId": "$id" }]
uiState = uiState.copy(error = e.message, isLoading = false)
}
}
}
}
// [ENTITY: DataClass('LabelEditUiState')]
/**
* @summary Состояние UI для экрана редактирования метки.
*/
data class LabelEditUiState(
val name: String = "",
val description: String? = null,
val color: String = "#FFFFFF", // Default color
val nameError: String? = null,
val isLoading: Boolean = false,
val error: String? = null,
val isSaved: Boolean = false,
val originalLabel: LabelOut? = null // To hold original label details if editing
)
// [END_ENTITY: DataClass('LabelEditUiState')]
// [END_FILE_LabelEditViewModel.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt // [FILE] LabelsListScreen.kt
// [SEMANTICS] app, ui, screen, list, label // [SEMANTICS] ui, labels_list, state_management, compose, dialog
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -16,28 +17,33 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.Label
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.Screen
import com.homebox.lens.ui.navigation.Screen
import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS] // [END_IMPORTS]
@@ -45,30 +51,39 @@ import timber.log.Timber
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')] // [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')]
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')] // [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
/** /**
* @summary Displays the screen with a list of all labels. * @summary Отображает экран со списком всех меток.
* @param currentRoute The current navigation route. * @param navController Контроллер навигации для перемещения между экранами.
* @param navigationActions The object containing navigation actions. * @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
* @param viewModel The ViewModel providing the UI state for the labels screen.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabelsListScreen( fun LabelsListScreen(
currentRoute: String?, navController: NavController,
navigationActions: NavigationActions,
viewModel: LabelsListViewModel = hiltViewModel() viewModel: LabelsListViewModel = hiltViewModel()
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
MainScaffold(
topBarTitle = stringResource(id = R.string.screen_title_labels),
currentRoute = currentRoute,
navigationActions = navigationActions
) { paddingValues ->
Scaffold( Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
navigationIcon = {
IconButton(onClick = {
Timber.i("[INFO][ACTION][navigate_up] Navigate up initiated.")
navController.navigateUp()
}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
)
}
}
)
},
floatingActionButton = { floatingActionButton = {
FloatingActionButton(onClick = { FloatingActionButton(onClick = {
Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.") Timber.i("[INFO][ACTION][show_create_dialog] FAB clicked: Initiate create new label flow.")
navigationActions.navigateToLabelEdit(null) viewModel.onShowCreateDialog()
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
@@ -76,31 +91,42 @@ fun LabelsListScreen(
) )
} }
} }
) { innerPaddingValues -> ) { paddingValues ->
val currentState = uiState val currentState = uiState
if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) {
CreateLabelDialog(
onConfirm = { labelName ->
viewModel.createLabel(labelName)
},
onDismiss = {
viewModel.onDismissCreateDialog()
}
)
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPaddingValues), // Use innerPaddingValues here .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (val state = uiState) { when (currentState) {
is LabelsListUiState.Loading -> { is LabelsListUiState.Loading -> {
CircularProgressIndicator() CircularProgressIndicator()
} }
is LabelsListUiState.Error -> { is LabelsListUiState.Error -> {
Text(text = state.message) Text(text = currentState.message)
} }
is LabelsListUiState.Success -> { is LabelsListUiState.Success -> {
if (state.labels.isEmpty()) { if (currentState.labels.isEmpty()) {
Text(text = stringResource(id = R.string.no_labels_found)) Text(text = stringResource(id = R.string.labels_list_empty))
} else { } else {
LabelsList( LabelsList(
labels = state.labels, labels = currentState.labels,
onLabelClick = { label -> onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.") Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.")
navigationActions.navigateToLabelEdit(label.id) val route = Screen.InventoryList.withFilter("label", label.id)
navController.navigate(route)
} }
) )
} }
@@ -109,16 +135,15 @@ fun LabelsListScreen(
} }
} }
} }
}
// [END_ENTITY: Function('LabelsListScreen')] // [END_ENTITY: Function('LabelsListScreen')]
// [ENTITY: Function('LabelsList')] // [ENTITY: Function('LabelsList')]
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary Composable function for displaying a list of labels. * @summary Composable-функция для отображения списка меток.
* @param labels The list of `Label` objects to display. * @param labels Список объектов `Label` для отображения.
* @param onLabelClick A lambda function called when a list item is clicked. * @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
* @param modifier A modifier for customizing the appearance. * @param modifier Модификатор для настройки внешнего вида.
*/ */
@Composable @Composable
private fun LabelsList( private fun LabelsList(
@@ -144,9 +169,9 @@ private fun LabelsList(
// [ENTITY: Function('LabelListItem')] // [ENTITY: Function('LabelListItem')]
// [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary Composable function for displaying a single item in the list of labels. * @summary Composable-функция для отображения одного элемента в списке меток.
* @param label The `Label` object to display. * @param label Объект `Label`, который нужно отобразить.
* @param onClick A lambda function called when the item is clicked. * @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
*/ */
@Composable @Composable
private fun LabelListItem( private fun LabelListItem(
@@ -166,4 +191,46 @@ private fun LabelListItem(
} }
// [END_ENTITY: Function('LabelListItem')] // [END_ENTITY: Function('LabelListItem')]
// [ENTITY: Function('CreateLabelDialog')]
/**
* @summary Диалоговое окно для создания новой метки.
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
*/
@Composable
private fun CreateLabelDialog(
onConfirm: (String) -> Unit,
onDismiss: () -> Unit
) {
var text by remember { mutableStateOf("") }
val isConfirmEnabled = text.isNotBlank()
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
text = {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text(stringResource(R.string.dialog_field_label_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(
onClick = { onConfirm(text) },
enabled = isConfirmEnabled
) {
Text(stringResource(R.string.dialog_button_create))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.dialog_button_cancel))
}
}
)
}
// [END_ENTITY: Function('CreateLabelDialog')]
// [END_FILE_LabelsListScreen.kt] // [END_FILE_LabelsListScreen.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListUiState.kt // [FILE] LabelsListUiState.kt
// [SEMANTICS] app, ui, state, list, label // [SEMANTICS] ui_state, sealed_interface, contract
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -8,17 +9,17 @@ import com.homebox.lens.domain.model.Label
// [ENTITY: SealedInterface('LabelsListUiState')] // [ENTITY: SealedInterface('LabelsListUiState')]
/** /**
* @summary Defines all possible states for the UI of the screen with a list of labels. * @summary Определяет все возможные состояния для UI экрана со списком меток.
* @description Using a sealed interface allows for exhaustive handling of all states in Composable functions. * @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
*/ */
sealed interface LabelsListUiState { sealed interface LabelsListUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary The success state, contains the list of labels and the state of the dialog. * @summary Состояние успеха, содержит список меток и состояние диалога.
* @param labels The list of labels to display. * @param labels Список меток для отображения.
* @param isShowingCreateDialog A flag indicating whether the label creation dialog should be displayed. * @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
* @invariant labels cannot be null. * @invariant labels не может быть null.
*/ */
data class Success( data class Success(
val labels: List<Label>, val labels: List<Label>,
@@ -28,17 +29,17 @@ sealed interface LabelsListUiState {
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary The error state. * @summary Состояние ошибки.
* @param message The error text to display to the user. * @param message Текст ошибки для отображения пользователю.
* @invariant message cannot be empty. * @invariant message не может быть пустой.
*/ */
data class Error(val message: String) : LabelsListUiState data class Error(val message: String) : LabelsListUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary The data loading state. * @summary Состояние загрузки данных.
* @description Indicates that the process of loading labels is in progress. * @description Указывает, что идет процесс загрузки меток.
*/ */
data object Loading : LabelsListUiState data object Loading : LabelsListUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt // [FILE] LabelsListViewModel.kt
// [SEMANTICS] app, ui, viewmodel, list, label // [SEMANTICS] ui_logic, labels_list, state_management, dialog_management
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -20,9 +21,9 @@ import javax.inject.Inject
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')] // [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')] // [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
/** /**
* @summary ViewModel for the screen with a list of labels. * @summary ViewModel для экрана со списком меток.
* @description Manages the screen state, loads the list of labels, handles errors, and manages the dialog for creating a new label. * @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
* @invariant `uiState` is always one of the states defined in `LabelsListUiState`. * @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/ */
@HiltViewModel @HiltViewModel
class LabelsListViewModel @Inject constructor( class LabelsListViewModel @Inject constructor(
@@ -38,10 +39,10 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('loadLabels')] // [ENTITY: Function('loadLabels')]
/** /**
* @summary Loads the list of labels. * @summary Загружает список меток.
* @description Executes `GetAllLabelsUseCase` and updates the UI by switching it * @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
* between the `Loading`, `Success`, and `Error` states. * между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Asynchronously updates `_uiState`. * @sideeffect Асинхронно обновляет `_uiState`.
*/ */
fun loadLabels() { fun loadLabels() {
viewModelScope.launch { viewModelScope.launch {
@@ -76,9 +77,9 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('onShowCreateDialog')] // [ENTITY: Function('onShowCreateDialog')]
/** /**
* @summary Initiates the display of the dialog for creating a label. * @summary Инициирует отображение диалога для создания метки.
* @description Updates the `uiState` by setting `isShowingCreateDialog` to `true`. * @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
* @sideeffect Updates `_uiState`. * @sideeffect Обновляет `_uiState`.
*/ */
fun onShowCreateDialog() { fun onShowCreateDialog() {
Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.") Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.")
@@ -92,9 +93,9 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('onDismissCreateDialog')] // [ENTITY: Function('onDismissCreateDialog')]
/** /**
* @summary Hides the label creation dialog. * @summary Скрывает диалог создания метки.
* @description Updates the `uiState` by setting `isShowingCreateDialog` to `false`. * @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
* @sideeffect Updates `_uiState`. * @sideeffect Обновляет `_uiState`.
*/ */
fun onDismissCreateDialog() { fun onDismissCreateDialog() {
Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.") Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.")
@@ -108,12 +109,12 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('createLabel')] // [ENTITY: Function('createLabel')]
/** /**
* @summary Creates a new label. [MVP_SCOPE] STUB. * @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
* @description In the current implementation (Plan B, Stage 1), this function only logs the action * @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
* and hides the dialog. The actual save logic will be added in the next stage. * и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
* @param name The name of the new label. * @param name Название новой метки.
* @precondition `name` must not be blank. * @precondition `name` не должен быть пустым.
* @sideeffect Logs the action, updates `_uiState` to hide the dialog. * @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
*/ */
fun createLabel(name: String) { fun createLabel(name: String) {
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." } require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationedit
// [FILE] LocationEditScreen.kt // [FILE] LocationEditScreen.kt
// [SEMANTICS] app, ui, screen, edit, location // [SEMANTICS] ui, screen, location, edit
package com.homebox.lens.ui.screen.locationedit package com.homebox.lens.ui.screen.locationedit
@@ -18,8 +19,8 @@ import com.homebox.lens.R
// [ENTITY: Function('LocationEditScreen')] // [ENTITY: Function('LocationEditScreen')]
/** /**
* @summary Composable function for the "Edit Location" screen. * @summary Composable-функция для экрана "Редактирование местоположения".
* @param locationId The ID of the location to edit, or "new" to create one. * @param locationId ID местоположения для редактирования или "new" для создания.
*/ */
@Composable @Composable
fun LocationEditScreen( fun LocationEditScreen(

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt // [FILE] LocationsListScreen.kt
// [SEMANTICS] app, ui, screen, list, location // [SEMANTICS] ui, screen, locations, list
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -55,12 +56,12 @@ import com.homebox.lens.ui.theme.HomeboxLensTheme
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable function for the "List of Locations" screen. * @summary Composable-функция для экрана "Список местоположений".
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
* @param onLocationClick A lambda handler for clicking on a location. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onAddNewLocationClick A lambda handler for clicking the button to add a new location. * @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
* @param viewModel The ViewModel for this screen. * @param viewModel ViewModel для этого экрана.
*/ */
@Composable @Composable
fun LocationsListScreen( fun LocationsListScreen(
@@ -103,12 +104,12 @@ fun LocationsListScreen(
// [ENTITY: Function('LocationsListContent')] // [ENTITY: Function('LocationsListContent')]
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')] // [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
/** /**
* @summary Displays the main content of the screen depending on the `uiState`. * @summary Отображает основной контент экрана в зависимости от `uiState`.
* @param modifier A modifier for styling. * @param modifier Модификатор для стилизации.
* @param uiState The current UI state. * @param uiState Текущее состояние UI.
* @param onLocationClick A lambda handler for clicking on a location. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onEditLocation A lambda handler for editing a location. * @param onEditLocation Лямбда-обработчик для редактирования местоположения.
* @param onDeleteLocation A lambda handler for deleting a location. * @param onDeleteLocation Лямбда-обработчик для удаления местоположения.
*/ */
@Composable @Composable
private fun LocationsListContent( private fun LocationsListContent(
@@ -166,11 +167,11 @@ private fun LocationsListContent(
// [ENTITY: Function('LocationCard')] // [ENTITY: Function('LocationCard')]
// [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary Card for displaying a single location. * @summary Карточка для отображения одного местоположения.
* @param location The data about the location. * @param location Данные о местоположении.
* @param onClick A lambda handler for clicking on the card. * @param onClick Лямбда-обработчик нажатия на карточку.
* @param onEditClick A lambda handler for clicking "Edit". * @param onEditClick Лямбда-обработчик нажатия на "Редактировать".
* @param onDeleteClick A lambda handler for clicking "Delete". * @param onDeleteClick Лямбда-обработчик нажатия на "Удалить".
*/ */
@Composable @Composable
private fun LocationCard( private fun LocationCard(

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListUiState.kt // [FILE] LocationsListUiState.kt
// [SEMANTICS] app, ui, state, list, location // [SEMANTICS] ui, state, locations
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -9,30 +10,30 @@ import com.homebox.lens.domain.model.LocationOutCount
// [ENTITY: SealedInterface('LocationsListUiState')] // [ENTITY: SealedInterface('LocationsListUiState')]
/** /**
* @summary Defines the possible UI states for the list of locations screen. * @summary Определяет возможные состояния UI для экрана списка местоположений.
* @see LocationsListViewModel * @see LocationsListViewModel
*/ */
sealed interface LocationsListUiState { sealed interface LocationsListUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary The state of a successful data load. * @summary Состояние успешной загрузки данных.
* @param locations The list of locations to display. * @param locations Список местоположений для отображения.
*/ */
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
// [END_ENTITY: DataClass('Success')] // [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary The error state. * @summary Состояние ошибки.
* @param message The error message. * @param message Сообщение об ошибке.
*/ */
data class Error(val message: String) : LocationsListUiState data class Error(val message: String) : LocationsListUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary The data loading state. * @summary Состояние загрузки данных.
*/ */
object Loading : LocationsListUiState object Loading : LocationsListUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt // [FILE] LocationsListViewModel.kt
// [SEMANTICS] app, ui, viewmodel, list, location // [SEMANTICS] ui, viewmodel, locations, hilt
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -20,10 +21,10 @@ import javax.inject.Inject
// [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')] // [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
// [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')] // [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')]
/** /**
* @summary ViewModel for the list of locations screen. * @summary ViewModel для экрана списка местоположений.
* @param getAllLocationsUseCase Use case for getting all locations. * @param getAllLocationsUseCase Use case для получения всех местоположений.
* @property uiState A flow containing the current UI state. * @property uiState Поток, содержащий текущее состояние UI.
* @invariant `uiState` always reflects the result of the last load operation. * @invariant `uiState` всегда отражает результат последней операции загрузки.
*/ */
@HiltViewModel @HiltViewModel
class LocationsListViewModel @Inject constructor( class LocationsListViewModel @Inject constructor(
@@ -39,8 +40,8 @@ class LocationsListViewModel @Inject constructor(
// [ENTITY: Function('loadLocations')] // [ENTITY: Function('loadLocations')]
/** /**
* @summary Loads the list of locations from the repository. * @summary Загружает список местоположений из репозитория.
* @sideeffect Updates `_uiState` depending on the result: Loading -> Success/Error. * @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
*/ */
fun loadLocations() { fun loadLocations() {
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.") Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt // [FILE] SearchScreen.kt
// [SEMANTICS] app, ui, screen, search // [SEMANTICS] ui, screen, search
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
@@ -16,9 +17,9 @@ import com.homebox.lens.ui.common.MainScaffold
// [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable function for the "Search" screen. * @summary Composable-функция для экрана "Поиск".
* @param currentRoute The current route to highlight the active item in the Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions The object with navigation actions. * @param navigationActions Объект с навигационными действиями.
*/ */
@Composable @Composable
fun SearchScreen( fun SearchScreen(

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt // [FILE] SearchViewModel.kt
// [SEMANTICS] app, ui, viewmodel, search // [SEMANTICS] ui, viewmodel, search
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
// [IMPORTS] // [IMPORTS]

View File

@@ -1,52 +0,0 @@
// [FILE] SettingsScreen.kt
// [SEMANTICS] app, ui, screen, settings
package com.homebox.lens.ui.screen.settings
// [IMPORTS]
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('SettingsScreen')]
// [RELATION: Function('SettingsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
/**
* @summary Composable function for the settings screen.
* @param currentRoute The current navigation route.
* @param navigationActions The object containing navigation actions.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.screen_title_settings),
currentRoute = currentRoute,
navigationActions = navigationActions
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Text(text = "Settings Screen (Under Construction)")
}
}
}
// [END_ENTITY: Function('SettingsScreen')]
// [END_FILE_SettingsScreen.kt]

View File

@@ -1,27 +1,23 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt // [FILE] SetupScreen.kt
// [SEMANTICS] app, ui, screen, setup // [SEMANTICS] ui, screen, setup, compose
@file:OptIn(ExperimentalMaterial3Api::class) @file:OptIn(ExperimentalMaterial3Api::class)
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
// [END_IMPORTS] // [END_IMPORTS]
@@ -30,10 +26,10 @@ import com.homebox.lens.R
// [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')] // [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
// [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')] // [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')]
/** /**
* @summary The main Composable function for the server connection setup screen. * @summary Главная Composable-функция для экрана настройки соединения с сервером.
* @param viewModel The ViewModel for this screen, provided by Hilt. * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param onSetupComplete A lambda invoked after successful setup and login. * @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
* @sideeffect Calls `onSetupComplete` when `uiState.isSetupComplete` changes. * @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
*/ */
@Composable @Composable
fun SetupScreen( fun SetupScreen(
@@ -59,12 +55,12 @@ fun SetupScreen(
// [ENTITY: Function('SetupScreenContent')] // [ENTITY: Function('SetupScreenContent')]
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')] // [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
/** /**
* @summary Displays the content of the setup screen: input fields and a button. * @summary Отображает контент экрана настройки: поля ввода и кнопку.
* @param uiState The current UI state. * @param uiState Текущее состояние UI.
* @param onServerUrlChange A lambda handler for changing the server URL. * @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
* @param onUsernameChange A lambda handler for changing the username. * @param onUsernameChange Лямбда-обработчик изменения имени пользователя.
* @param onPasswordChange A lambda handler for changing the password. * @param onPasswordChange Лямбда-обработчик изменения пароля.
* @param onConnectClick A lambda handler for clicking the "Connect" button. * @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться".
*/ */
@Composable @Composable
private fun SetupScreenContent( private fun SetupScreenContent(
@@ -74,7 +70,11 @@ private fun SetupScreenContent(
onPasswordChange: (String) -> Unit, onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit onConnectClick: () -> Unit
) { ) {
Scaffold { paddingValues -> Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(id = R.string.setup_title)) })
}
) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -82,30 +82,6 @@ private fun SetupScreenContent(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
imageVector = Icons.Default.Lock,
contentDescription = stringResource(id = R.string.app_name),
modifier = Modifier.size(128.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.setup_title),
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter your Homebox server details to connect.",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) { ) {
OutlinedTextField( OutlinedTextField(
value = uiState.serverUrl, value = uiState.serverUrl,
@@ -128,31 +104,21 @@ private fun SetupScreenContent(
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(24.dp))
Button( Button(
onClick = onConnectClick, onClick = onConnectClick,
enabled = !uiState.isLoading, enabled = !uiState.isLoading,
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.height(50.dp)
) { ) {
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator( CircularProgressIndicator(modifier = Modifier.size(24.dp))
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else { } else {
Text(stringResource(id = R.string.setup_connect_button)) Text(stringResource(id = R.string.setup_connect_button))
} }
} }
uiState.error?.let { uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(text = it, color = MaterialTheme.colorScheme.error)
text = it,
color = MaterialTheme.colorScheme.error
)
} }
} }
} }

View File

@@ -1,3 +1,4 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt // [FILE] SetupUiState.kt
// [SEMANTICS] ui_state, data_model, immutable // [SEMANTICS] ui_state, data_model, immutable

View File

@@ -1,3 +1,4 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt // [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow // [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
@@ -73,33 +74,12 @@ class SetupViewModel @Inject constructor(
// [END_ENTITY: Function('onUsernameChange')] // [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')] // [ENTITY: Function('onPasswordChange')]
/**
* @summary Updates the password in the UI state.
* @param newPassword The new password.
* @sideeffect Updates the `password` in `_uiState`.
*/
fun onPasswordChange(newPassword: String) { fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) } _uiState.update { it.copy(password = newPassword) }
} }
// [END_ENTITY: Function('onPasswordChange')] // [END_ENTITY: Function('onPasswordChange')]
// [ENTITY: Function('areCredentialsSaved')]
/**
* @summary Checks synchronously if credentials are saved.
* @return true if credentials are saved, false otherwise.
* @sideeffect None.
*/
fun areCredentialsSaved(): Boolean {
Timber.d("[DEBUG][ENTRYPOINT][checking_credentials_saved] Checking if credentials are saved.")
return credentialsRepository.areCredentialsSavedSync()
}
// [END_ENTITY: Function('areCredentialsSaved')]
// [ENTITY: Function('connect')] // [ENTITY: Function('connect')]
/**
* @summary Initiates the connection process, saving credentials and attempting to log in.
* @sideeffect Updates `_uiState` with loading, error, and completion states.
*/
fun connect() { fun connect() {
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.") Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
viewModelScope.launch { viewModelScope.launch {

View File

@@ -1,60 +0,0 @@
// [FILE] SplashScreen.kt
// [SEMANTICS] app, ui, screen, splash
package com.homebox.lens.ui.screen.splash
// [IMPORTS]
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.homebox.lens.ui.navigation.Screen
import com.homebox.lens.ui.screen.setup.SetupViewModel
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Composable('SplashScreen')]
// [RELATION: Composable('SplashScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
// [RELATION: Composable('SplashScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
/**
* @summary A splash screen that checks for saved credentials and navigates accordingly.
* @param navController The navigation controller for navigating to the next screen.
* @param viewModel The view model for checking credentials.
* @sideeffect Navigates to either the Setup or Dashboard screen.
*/
@Composable
fun SplashScreen(
navController: NavController,
viewModel: SetupViewModel = hiltViewModel()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
LaunchedEffect(Unit) {
Timber.d("[DEBUG][ACTION][checking_credentials] Checking for saved credentials on splash screen.")
val areCredentialsSaved = viewModel.areCredentialsSaved()
val destination = if (areCredentialsSaved) {
Timber.d("[DEBUG][SUCCESS][credentials_found] Credentials found, navigating to Dashboard.")
Screen.Dashboard.route
} else {
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, navigating to Setup.")
Screen.Setup.route
}
navController.navigate(destination) {
popUpTo(Screen.Splash.route) {
inclusive = true
}
}
}
}
// [END_ENTITY: Composable('SplashScreen')]
// [END_FILE_SplashScreen.kt]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt // [FILE] Color.kt
// [SEMANTICS] app, ui, theme, color // [SEMANTICS] ui, theme, color
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt // [FILE] Theme.kt
// [SEMANTICS] app, ui, theme // [SEMANTICS] ui, theme
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -1,5 +1,6 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt // [FILE] Typography.kt
// [SEMANTICS] app, ui, theme, typography // [SEMANTICS] ui, theme, typography
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -0,0 +1,83 @@
<resources>
<string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Create</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
<string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="cd_add_new_label">Add new label</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Labels</string>
<string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation -->
<string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
<!-- Screen Titles -->
<string name="inventory_list_title">Inventory</string>
<!-- Screen Titles -->
<string name="item_details_title">Details</string>
<string name="item_edit_title">Edit Item</string>
<string name="labels_list_title">Labels</string>
<string name="locations_list_title">Locations</string>
<string name="search_title">Search</string>
<string name="save_item">Save</string>
<string name="item_name">Name</string>
<string name="item_description">Description</string>
<string name="item_quantity">Quantity</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Create Location</string>
<string name="location_edit_title_edit">Edit Location</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Items: %1$d</string>
<string name="cd_more_options">More options</string>
<!-- Setup Screen -->
<string name="setup_title">Server Setup</string>
<string name="setup_server_url_label">Server URL</string>
<string name="setup_username_label">Username</string>
<string name="setup_password_label">Password</string>
<string name="setup_connect_button">Connect</string>
<!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string>
<string name="content_desc_navigate_back">Navigate back</string>
<string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Label icon</string>
<string name="labels_list_empty">Labels not created yet.</string>
<string name="dialog_title_create_label">Create Label</string>
<string name="dialog_field_label_name">Label Name</string>
<string name="dialog_button_create">Create</string>
<string name="dialog_button_cancel">Cancel</string>
</resources>

View File

@@ -2,149 +2,80 @@
<string name="app_name">Homebox Lens</string> <string name="app_name">Homebox Lens</string>
<!-- Common --> <!-- Common -->
<string name="create">Create</string> <string name="create">Создать</string>
<string name="edit">Edit</string> <string name="edit">Редактировать</string>
<string name="delete">Delete</string> <string name="delete">Удалить</string>
<string name="search">Search</string> <string name="search">Поиск</string>
<string name="logout">Logout</string> <string name="logout">Выйти</string>
<string name="no_location">No location</string> <string name="no_location">Нет локации</string>
<string name="items_not_found">Items not found</string> <string name="items_not_found">Элементы не найдены</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string> <string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions --> <!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string> <string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Scan QR code</string> <string name="cd_scan_qr_code">Сканировать QR-код</string>
<string name="cd_navigate_back">Navigate back</string> <string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_add_new_location">Add new location</string> <string name="cd_add_new_location">Добавить новую локацию</string>
<string name="content_desc_add_label">Add new label</string> <string name="cd_add_new_label">Добавить новую метку</string>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string> <string name="dashboard_title">Главная</string>
<string name="dashboard_section_quick_stats">Quick Stats</string> <string name="dashboard_section_quick_stats">Быстрая статистика</string>
<string name="dashboard_section_recently_added">Recently Added</string> <string name="dashboard_section_recently_added">Недавно добавлено</string>
<string name="dashboard_section_locations">Locations</string> <string name="dashboard_section_locations">Места хранения</string>
<string name="dashboard_section_labels">Labels</string> <string name="dashboard_section_labels">Метки</string>
<string name="location_chip_label">%1$s (%2$d)</string> <string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics --> <!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string> <string name="dashboard_stat_total_items">Всего вещей</string>
<string name="dashboard_stat_total_value">Total Value</string> <string name="dashboard_stat_total_value">Общая стоимость</string>
<string name="dashboard_stat_total_labels">Total Labels</string> <string name="dashboard_stat_total_labels">Всего меток</string>
<string name="dashboard_stat_total_locations">Total Locations</string> <string name="dashboard_stat_total_locations">Всего локаций</string>
<!-- Navigation --> <!-- Navigation -->
<string name="nav_locations">Locations</string> <string name="nav_locations">Локации</string>
<string name="nav_labels">Labels</string> <string name="nav_labels">Метки</string>
<!-- Screen Titles --> <!-- Screen Titles -->
<string name="inventory_list_title">Inventory</string> <string name="inventory_list_title">Инвентарь</string>
<string name="item_details_title">Детали</string>
<string name="item_edit_title">Редактирование</string>
<string name="labels_list_title">Метки</string>
<string name="locations_list_title">Места хранения</string>
<string name="search_title">Поиск</string>
<!-- Screen Titles --> <string name="save_item">Сохранить</string>
<string name="item_details_title">Details</string> <string name="item_name">Название</string>
<string name="item_edit_title">Edit Item</string> <string name="item_description">Описание</string>
<string name="labels_list_title">Labels</string> <string name="item_quantity">Количество</string>
<string name="locations_list_title">Locations</string>
<string name="search_title">Search</string>
<string name="save_item">Save</string>
<string name="item_name">Name</string>
<string name="item_description">Description</string>
<string name="item_quantity">Quantity</string>
<!-- Location Edit Screen --> <!-- Location Edit Screen -->
<string name="location_edit_title_create">Create Location</string> <string name="location_edit_title_create">Создать локацию</string>
<string name="location_edit_title_edit">Edit Location</string> <string name="location_edit_title_edit">Редактировать локацию</string>
<!-- Locations List Screen --> <!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string> <string name="locations_not_found">Местоположения не найдены. Нажмите +, чтобы добавить новое.</string>
<string name="item_count">Items: %1$d</string> <string name="item_count">Предметов: %1$d</string>
<string name="cd_more_options">More options</string> <string name="cd_more_options">Больше опций</string>
<!-- Setup Screen --> <!-- Setup Screen -->
<string name="setup_title">Server Setup</string> <string name="setup_title">Настройка сервера</string>
<string name="setup_server_url_label">Server URL</string> <string name="setup_server_url_label">URL сервера</string>
<string name="setup_username_label">Username</string> <string name="setup_username_label">Имя пользователя</string>
<string name="setup_password_label">Password</string> <string name="setup_password_label">Пароль</string>
<string name="setup_connect_button">Connect</string> <string name="setup_connect_button">Подключиться</string>
<!-- Labels List Screen --> <!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string> <string name="screen_title_labels">Метки</string>
<string name="content_desc_navigate_back">Navigate back</string> <string name="content_desc_navigate_back">Вернуться назад</string>
<string name="content_desc_create_label">Create new label</string> <string name="content_desc_create_label">Создать новую метку</string>
<string name="content_desc_label_icon">Label icon</string> <string name="content_desc_label_icon">Иконка метки</string>
<string name="no_labels_found">No labels found.</string> <string name="labels_list_empty">Метки еще не созданы.</string>
<string name="dialog_title_create_label">Create Label</string> <string name="dialog_title_create_label">Создать метку</string>
<string name="dialog_field_label_name">Label Name</string> <string name="dialog_field_label_name">Название метки</string>
<string name="dialog_button_create">Create</string> <string name="dialog_button_create">Создать</string>
<string name="dialog_button_cancel">Cancel</string> <string name="dialog_button_cancel">Отмена</string>
<!-- Inventory List Screen -->
<string name="content_desc_sync_inventory">Sync inventory</string>
<!-- Item Details Screen -->
<string name="content_desc_edit_item">Edit item</string>
<string name="content_desc_delete_item">Delete item</string>
<string name="section_title_description">Description</string>
<string name="placeholder_no_description">No description</string>
<string name="section_title_details">Details</string>
<string name="label_quantity">Quantity</string>
<string name="label_location">Location</string>
<string name="section_title_labels">Labels</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Create item</string>
<string name="content_desc_save_item">Save item</string>
<string name="label_name">Name</string>
<string name="label_description">Description</string>
<string name="item_edit_general_information">General Information</string>
<string name="item_edit_location">Location</string>
<string name="item_edit_labels">Labels</string>
<string name="item_edit_select_labels">Select Labels</string>
<string name="dialog_ok">OK</string>
<string name="dialog_cancel">Cancel</string>
<string name="item_edit_purchase_information">Purchase Information</string>
<string name="item_edit_purchase_price">Purchase Price</string>
<string name="item_edit_purchase_from">Purchase From</string>
<string name="item_edit_purchase_time">Purchase Time</string>
<string name="item_edit_select_date">Select Date</string>
<string name="item_edit_warranty_information">Warranty Information</string>
<string name="item_edit_lifetime_warranty">Lifetime Warranty</string>
<string name="item_edit_warranty_details">Warranty Details</string>
<string name="item_edit_warranty_expires">Warranty Expires</string>
<string name="item_edit_identification">Identification</string>
<string name="item_edit_asset_id">Asset ID</string>
<string name="item_edit_serial_number">Serial Number</string>
<string name="item_edit_manufacturer">Manufacturer</string>
<string name="item_edit_model_number">Model Number</string>
<string name="item_edit_status_notes">Status &amp; Notes</string>
<string name="item_edit_archived">Archived</string>
<string name="item_edit_insured">Insured</string>
<string name="item_edit_notes">Notes</string>
<string name="item_edit_sold_information">Sold Information</string>
<string name="item_edit_sold_price">Sold Price</string>
<string name="item_edit_sold_to">Sold To</string>
<string name="item_edit_sold_notes">Sold Notes</string>
<string name="item_edit_sold_time">Sold Time</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Search items...</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Setup</string>
<string name="screen_title_settings">Settings</string>
<!-- Label Edit Screen -->
<string name="label_edit_title_create">Create label</string>
<string name="label_edit_title_edit">Edit label</string>
<string name="label_name_edit">Label name</string>
<!-- Common Actions -->
<string name="back">Back</string>
<string name="save">Save</string>
<!-- Color Picker -->
<string name="label_color">Color</string>
<string name="label_hex_color">HEX color code</string>
</resources> </resources>

View File

@@ -0,0 +1,126 @@
package com.homebox.lens.ui.screen.itemedit
import app.cash.turbine.test
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.UUID
@ExperimentalCoroutinesApi
class ItemEditViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var createItemUseCase: CreateItemUseCase
private lateinit var updateItemUseCase: UpdateItemUseCase
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
private lateinit var viewModel: ItemEditViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
createItemUseCase = mockk()
updateItemUseCase = mockk()
getItemDetailsUseCase = mockk()
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loadItem with valid id should update uiState with item`() = runTest {
val itemId = UUID.randomUUID().toString()
val itemOut = ItemOut(id = itemId, name = "Test Item", description = "Description", quantity = 1, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Test Item", uiState.item?.name)
}
@Test
fun `loadItem with null id should prepare a new item`() = runTest {
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals("", uiState.item?.id)
assertEquals("", uiState.item?.name)
}
@Test
fun `saveItem should call createItemUseCase for new item`() = runTest {
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
coEvery { createItemUseCase(any()) } returns createdItemSummary
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("New Item")
viewModel.updateDescription("New Description")
viewModel.updateQuantity(2)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(createdItemSummary.id, uiState.item?.id)
}
@Test
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
val itemId = UUID.randomUUID().toString()
val updatedItemOut = ItemOut(id = itemId, name = "Updated Item", description = "Updated Description", quantity = 4, images = emptyList(), location = null, labels = emptyList(), value = 12.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(id = itemId, name = "Existing Item", description = "Existing Description", quantity = 3, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { updateItemUseCase(any()) } returns updatedItemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("Updated Item")
viewModel.updateDescription("Updated Description")
viewModel.updateQuantity(4)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Updated Item", uiState.item?.name)
assertEquals(4, uiState.item?.quantity)
}
}

View File

@@ -3,13 +3,11 @@
plugins { plugins {
// [PLUGIN] Android Application plugin // [PLUGIN] Android Application plugin
id("com.android.application") version "8.4.0" apply false id("com.android.application") version "8.11.1" apply false
// [PLUGIN] Kotlin Android plugin // [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.23" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false
// [PLUGIN] Hilt Android plugin // [PLUGIN] Hilt Android plugin
id("com.google.dagger.hilt.android") version "2.48.1" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false
// [PLUGIN] KSP plugin
id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false
} }
// [END_FILE_build.gradle.kts] // [END_FILE_build.gradle.kts]

View File

@@ -15,7 +15,7 @@ object Versions {
const val coroutines = "1.7.3" const val coroutines = "1.7.3"
// Jetpack Compose // Jetpack Compose
const val composeCompiler = "1.5.11" const val composeCompiler = "1.5.8"
const val composeBom = "2023.10.01" const val composeBom = "2023.10.01"
const val activityCompose = "1.8.2" const val activityCompose = "1.8.2"
const val navigationCompose = "2.7.6" const val navigationCompose = "2.7.6"

View File

@@ -6,7 +6,6 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
id("com.google.devtools.ksp")
} }
android { android {
@@ -28,11 +27,11 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "1.8"
} }
} }
@@ -52,7 +51,7 @@ dependencies {
implementation(Libs.okhttp) implementation(Libs.okhttp)
implementation(Libs.okhttpLoggingInterceptor) implementation(Libs.okhttpLoggingInterceptor)
implementation(Libs.moshiKotlin) implementation(Libs.moshiKotlin)
ksp(Libs.moshiCodegen) kapt(Libs.moshiCodegen)
// [DEPENDENCY] Database (Room) // [DEPENDENCY] Database (Room)
implementation(Libs.roomRuntime) implementation(Libs.roomRuntime)

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,18 +0,0 @@
// Файл: /data/semantic-ktlint-rules/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
// Зависимость для RuleSetProviderV3
implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.2.1")
// Зависимость для Rule, RuleId и psi-утилит
api("com.pinterest.ktlint:ktlint-rule-engine:1.2.1")
// Зависимости для тестирования остаются без изменений
testImplementation(kotlin("test"))
testImplementation("com.pinterest.ktlint:ktlint-test:1.2.1")
testImplementation("org.assertj:assertj-core:3.24.2")
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,27 +0,0 @@
// [FILE] ExampleInstrumentedTest.kt
// [SEMANTICS] testing, android, ktlint, rules
package com.busya.ktlint.rules
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.busya.ktlint.rules", appContext.packageName)
}
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HomeboxLens" />
</manifest>

View File

@@ -1,17 +0,0 @@
// [FILE] CustomRuleSetProvider.kt
// [SEMANTICS] ktlint, rules, provider
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId("custom")) {
override fun getRuleProviders(): Set<RuleProvider> {
return setOf(
RuleProvider { FileHeaderRule() },
RuleProvider { MandatoryEntityDeclarationRule() },
RuleProvider { NoStrayCommentsRule() }
)
}
}

View File

@@ -1,34 +0,0 @@
// [FILE] FileHeaderRule.kt
// [SEMANTICS] ktlint, rules, file_header
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class FileHeaderRule : Rule(ruleId = RuleId("custom:file-header-rule"), about = About()) {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.FILE) {
val lines = node.text.lines()
if (lines.size < 3) {
emit(node.startOffset, "File must start with a 3-line semantic header.", false)
return
}
if (!lines[0].startsWith("// [PACKAGE]")) {
emit(node.startOffset, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.", false)
}
if (!lines[1].startsWith("// [FILE]")) {
emit(node.startOffset + lines[0].length + 1, "File header missing or incorrect. Line 2 must be '// [FILE] ...'.", false)
}
if (!lines[2].startsWith("// [SEMANTICS]")) {
emit(node.startOffset + lines[0].length + lines[1].length + 2, "File header missing or incorrect. Line 3 must be '// [SEMANTICS] ...'.", false)
}
}
}
}

View File

@@ -1,41 +0,0 @@
// [FILE] MandatoryEntityDeclarationRule.kt
// [SEMANTICS] ktlint, rules, entity_declaration
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
class MandatoryEntityDeclarationRule : Rule(ruleId = RuleId("custom:entity-declaration-rule"), about = About()) {
private val entityTypes = setOf(
ElementType.CLASS,
ElementType.OBJECT_DECLARATION,
ElementType.FUN
)
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType in entityTypes) {
val ktDeclaration = node.psi as? KtDeclaration ?: return
if (node.elementType == ElementType.FUN &&
(ktDeclaration.hasModifier(KtTokens.PRIVATE_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.PROTECTED_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.INTERNAL_KEYWORD))
) {
return
}
val prevComment = node.prevLeaf { it.elementType == ElementType.EOL_COMMENT }
if (prevComment == null || !prevComment.text.startsWith("// [ENTITY:")) {
emit(node.startOffset, "Missing or misplaced '// [ENTITY: ...]' declaration before '${node.elementType}'.", false)
}
}
}
}

View File

@@ -1,25 +0,0 @@
// [FILE] NoStrayCommentsRule.kt
// [SEMANTICS] ktlint, rules, comments
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class NoStrayCommentsRule : Rule(ruleId = RuleId("custom:no-stray-comments-rule"), about = About()) {
private val allowedCommentPattern = Regex("""^//\s?\[([A-Z_]+|ENTITY:|RELATION:|AI_NOTE:)]""")
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.EOL_COMMENT) {
val commentText = node.text
if (!allowedCommentPattern.matches(commentText)) {
emit(node.startOffset, "Stray comment found. Use semantic anchors like '// [TAG]' or '// [AI_NOTE]:' instead.", false)
}
}
}
}

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Some files were not shown because too many files have changed in this diff Show More