This commit is contained in:
2025-09-06 12:34:25 +03:00
parent c5ee179e71
commit 30ef449756
26 changed files with 860 additions and 207 deletions

View File

@@ -1,85 +1,26 @@
<GITEA_ISSUE_DRIVEN_PROTOCOL>
<META>
<PURPOSE>Определить единый, отказоустойчивый и полностью автоматизированный протокол для межагентной коммуникации, основанный на использовании высокоуровневого клиента 'gitea-client.zsh'.</PURPOSE>
<VERSION>4.0</VERSION>
</META>
<IMPLEMENTATION name="GiteaIssueTaskSource">
<IMPLEMENTS_INTERFACE type="TaskSource"/>
<USES_PROTOCOL name="GiteaIssueDrivenProtocol"/>
<CORE_PRINCIPLES>
<PRINCIPLE name="Abstraction_Is_Mandatory">
<DESCRIPTION>**КЛЮЧЕВОЕ ИЗМЕНЕНИЕ:** Все взаимодействия с Gitea **ОБЯЗАНЫ** осуществляться исключительно через `gitea-client.zsh`. Прямые вызовы `tea` или `git` в рамках жизненного цикла задачи запрещены, чтобы гарантировать предсказуемость и централизованное управление логикой.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Automated_Context_Discovery">
<DESCRIPTION>Клиент `gitea-client.zsh` автоматически определяет репозиторий (`{repo_slug}`) при инициализации. Агентам не нужно управлять этим состоянием. Роль (`{role_name}`) передается как первый аргумент при каждом вызове.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Human_Out_Of_The_Loop">
<DESCRIPTION>Человек взаимодействует с системой исключительно через диалог с Агентом-Архитектором, который инициирует весь воркфлоу.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Pull_Request_As_The_Unit_Of_Work">
<DESCRIPTION>Конечным продуктом работы Агента-Разработчика является формальный Pull Request (PR), который является основой для проверки и слияния.</DESCRIPTION>
</PRINCIPLE>
</CORE_PRINCIPLES>
<DESCRIPTION>
Реализует канал получения задач через поиск открытых issues в Gitea,
используя `gitea-client.zsh`.
</DESCRIPTION>
<CLIENT_API name="gitea-client.zsh">
<SYNTAX>`./gitea-client.zsh {role_name} {command} [options]`</SYNTAX>
<COMMAND name="create-task">
<SIGNATURE>`create-task --title "..." --body "..." --assignee "..." --labels "..."`</SIGNATURE>
<PURPOSE>Создание новой задачи в Gitea.</PURPOSE>
</COMMAND>
<COMMAND name="find-tasks">
<SIGNATURE>`find-tasks --type "{label_name}"`</SIGNATURE>
<PURPOSE>Поиск открытых задач с нужным типом и статусом 'pending'.</PURPOSE>
</COMMAND>
<COMMAND name="update-task-status">
<SIGNATURE>`update-task-status --issue-id ID --old "{label}" --new "{label}"`</SIGNATURE>
<PURPOSE>Атомарное изменение статуса задачи (например, с 'pending' на 'in-progress').</PURPOSE>
</COMMAND>
<COMMAND name="create-pr">
<SIGNATURE>`create-pr --title "..." --body "..." --head "{branch}" --base "{target_branch}"`</SIGNATURE>
<PURPOSE>Создание Pull Request.</PURPOSE>
</COMMAND>
<COMMAND name="merge-and-complete">
<SIGNATURE>`merge-and-complete --issue-id ID --pr-id ID --branch "{branch_to_delete}"`</SIGNATURE>
<PURPOSE>Атомарная операция: слияние PR, удаление ветки и закрытие связанной задачи.</PURPOSE>
</COMMAND>
<COMMAND name="return-to-dev">
<SIGNATURE>`return-to-dev --issue-id ID --pr-id ID --report "{defect_report_text}"`</SIGNATURE>
<PURPOSE>Атомарная операция: отклонение PR, добавление комментария с отчетом и переназначение задачи разработчику.</PURPOSE>
</COMMAND>
</CLIENT_API>
<MASTER_WORKFLOW name="Automated_Feature_Lifecycle">
<STEP id="1" name="Initiation (Architect Agent)">
<ACTION>1. Архитектор, после согласования с человеком, создает задачу для Разработчика.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-architect create-task --title "Реализовать модуль X" --body "..." --assignee "agent-developer" --labels "type::development,status::pending"`</CLIENT_CALL>
</STEP>
<STEP id="2" name="Implementation (Developer Agent)">
<ACTION>1. Разработчик находит назначенную ему задачу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer find-tasks --type "type::development"`</CLIENT_CALL>
<ACTION>2. Берет задачу в работу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"`</CLIENT_CALL>
<ACTION>3. После написания кода и локальных тестов создает Pull Request.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer create-pr --title "feat: Реализован модуль X" --body "Closes #{issue-id}" --head "feature/{issue-id}-module-x"`</CLIENT_CALL>
<ACTION>4. Создает задачу для QA-агента, передавая ему контекст (ID задачи и PR).</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer create-task --title "QA: Проверить реализацию модуля X" --body "PR: #{pr-id}\nIssue: #{issue-id}" --assignee "agent-qa" --labels "type::quality-assurance,status::pending"`</CLIENT_CALL>
</STEP>
<STEP id="3" name="Verification_And_Merge (QA Agent)">
<ACTION>1. QA-Агент находит свою задачу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa find-tasks --type "type::quality-assurance"`</CLIENT_CALL>
<ACTION>2. Берет задачу в работу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa update-task-status --issue-id {qa-issue-id} --old "status::pending" --new "status::in-progress"`</CLIENT_CALL>
<ACTION>3. Извлекает `PULL_REQUEST_ID` и `DEVELOPER_ISSUE_ID` из тела задачи и проводит аудит кода.</ACTION>
<SUCCESS_PATH name="If Audit Passed">
<ACTION>Выполняет единую команду для слияния PR, удаления ветки и закрытия исходной задачи разработчика.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa merge-and-complete --issue-id {developer-issue-id} --pr-id {pr-id} --branch "feature/{issue-id}-module-x"`</CLIENT_CALL>
</SUCCESS_PATH>
<FAILURE_PATH name="If Audit Failed">
<ACTION>Выполняет единую команду для отклонения PR и возврата задачи разработчику с отчетом.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa return-to-dev --issue-id {developer-issue-id} --pr-id {pr-id} --report "Найдены следующие дефекты: ..."`</CLIENT_CALL>
</FAILURE_PATH>
</STEP>
</MASTER_WORKFLOW>
</GITEA_ISSUE_DRIVEN_PROTOCOL>
<METHOD_IMPLEMENTATION name="GetNextPendingTask">
<INPUT>RoleName</INPUT>
<ACTION>
Выполнить команду `./gitea-client.zsh {RoleName} find-tasks --type "type::development"`
для поиска доступных задач для указанной роли.
</ACTION>
<ACTION>
Если найдена одна или несколько задач, взять первую из списка.
</ACTION>
<ACTION>
Извлечь содержимое задачи (WorkOrder) и вернуть его.
</ACTION>
<ACTION>
Если задачи не найдены, вернуть `NULL`.
</ACTION>
</METHOD_IMPLEMENTATION>
</IMPLEMENTATION>

View File

@@ -0,0 +1,17 @@
<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

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

View File

@@ -0,0 +1,85 @@
<GITEA_ISSUE_DRIVEN_PROTOCOL>
<META>
<PURPOSE>Определить единый, отказоустойчивый и полностью автоматизированный протокол для межагентной коммуникации, основанный на использовании высокоуровневого клиента 'gitea-client.zsh'.</PURPOSE>
<VERSION>4.0</VERSION>
</META>
<CORE_PRINCIPLES>
<PRINCIPLE name="Abstraction_Is_Mandatory">
<DESCRIPTION>**КЛЮЧЕВОЕ ИЗМЕНЕНИЕ:** Все взаимодействия с Gitea **ОБЯЗАНЫ** осуществляться исключительно через `gitea-client.zsh`. Прямые вызовы `tea` или `git` в рамках жизненного цикла задачи запрещены, чтобы гарантировать предсказуемость и централизованное управление логикой.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Automated_Context_Discovery">
<DESCRIPTION>Клиент `gitea-client.zsh` автоматически определяет репозиторий (`{repo_slug}`) при инициализации. Агентам не нужно управлять этим состоянием. Роль (`{role_name}`) передается как первый аргумент при каждом вызове.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Human_Out_Of_The_Loop">
<DESCRIPTION>Человек взаимодействует с системой исключительно через диалог с Агентом-Архитектором, который инициирует весь воркфлоу.</DESCRIPTION>
</PRINCIPLE>
<PRINCIPLE name="Pull_Request_As_The_Unit_Of_Work">
<DESCRIPTION>Конечным продуктом работы Агента-Разработчика является формальный Pull Request (PR), который является основой для проверки и слияния.</DESCRIPTION>
</PRINCIPLE>
</CORE_PRINCIPLES>
<CLIENT_API name="gitea-client.zsh">
<SYNTAX>`./gitea-client.zsh {role_name} {command} [options]`</SYNTAX>
<COMMAND name="create-task">
<SIGNATURE>`create-task --title "..." --body "..." --assignee "..." --labels "..."`</SIGNATURE>
<PURPOSE>Создание новой задачи в Gitea.</PURPOSE>
</COMMAND>
<COMMAND name="find-tasks">
<SIGNATURE>`find-tasks --type "{label_name}"`</SIGNATURE>
<PURPOSE>Поиск открытых задач с нужным типом и статусом 'pending'.</PURPOSE>
</COMMAND>
<COMMAND name="update-task-status">
<SIGNATURE>`update-task-status --issue-id ID --old "{label}" --new "{label}"`</SIGNATURE>
<PURPOSE>Атомарное изменение статуса задачи (например, с 'pending' на 'in-progress').</PURPOSE>
</COMMAND>
<COMMAND name="create-pr">
<SIGNATURE>`create-pr --title "..." --body "..." --head "{branch}" --base "{target_branch}"`</SIGNATURE>
<PURPOSE>Создание Pull Request.</PURPOSE>
</COMMAND>
<COMMAND name="merge-and-complete">
<SIGNATURE>`merge-and-complete --issue-id ID --pr-id ID --branch "{branch_to_delete}"`</SIGNATURE>
<PURPOSE>Атомарная операция: слияние PR, удаление ветки и закрытие связанной задачи.</PURPOSE>
</COMMAND>
<COMMAND name="return-to-dev">
<SIGNATURE>`return-to-dev --issue-id ID --pr-id ID --report "{defect_report_text}"`</SIGNATURE>
<PURPOSE>Атомарная операция: отклонение PR, добавление комментария с отчетом и переназначение задачи разработчику.</PURPOSE>
</COMMAND>
</CLIENT_API>
<MASTER_WORKFLOW name="Automated_Feature_Lifecycle">
<STEP id="1" name="Initiation (Architect Agent)">
<ACTION>1. Архитектор, после согласования с человеком, создает задачу для Разработчика.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-architect create-task --title "Реализовать модуль X" --body "..." --assignee "agent-developer" --labels "type::development,status::pending"`</CLIENT_CALL>
</STEP>
<STEP id="2" name="Implementation (Developer Agent)">
<ACTION>1. Разработчик находит назначенную ему задачу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer find-tasks --type "type::development"`</CLIENT_CALL>
<ACTION>2. Берет задачу в работу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"`</CLIENT_CALL>
<ACTION>3. После написания кода и локальных тестов создает Pull Request.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer create-pr --title "feat: Реализован модуль X" --body "Closes #{issue-id}" --head "feature/{issue-id}-module-x"`</CLIENT_CALL>
<ACTION>4. Создает задачу для QA-агента, передавая ему контекст (ID задачи и PR).</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-developer create-task --title "QA: Проверить реализацию модуля X" --body "PR: #{pr-id}\nIssue: #{issue-id}" --assignee "agent-qa" --labels "type::quality-assurance,status::pending"`</CLIENT_CALL>
</STEP>
<STEP id="3" name="Verification_And_Merge (QA Agent)">
<ACTION>1. QA-Агент находит свою задачу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa find-tasks --type "type::quality-assurance"`</CLIENT_CALL>
<ACTION>2. Берет задачу в работу.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa update-task-status --issue-id {qa-issue-id} --old "status::pending" --new "status::in-progress"`</CLIENT_CALL>
<ACTION>3. Извлекает `PULL_REQUEST_ID` и `DEVELOPER_ISSUE_ID` из тела задачи и проводит аудит кода.</ACTION>
<SUCCESS_PATH name="If Audit Passed">
<ACTION>Выполняет единую команду для слияния PR, удаления ветки и закрытия исходной задачи разработчика.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa merge-and-complete --issue-id {developer-issue-id} --pr-id {pr-id} --branch "feature/{issue-id}-module-x"`</CLIENT_CALL>
</SUCCESS_PATH>
<FAILURE_PATH name="If Audit Failed">
<ACTION>Выполняет единую команду для отклонения PR и возврата задачи разработчику с отчетом.</ACTION>
<CLIENT_CALL>`./gitea-client.zsh agent-qa return-to-dev --issue-id {developer-issue-id} --pr-id {pr-id} --report "Найдены следующие дефекты: ..."`</CLIENT_CALL>
</FAILURE_PATH>
</STEP>
</MASTER_WORKFLOW>
</GITEA_ISSUE_DRIVEN_PROTOCOL>

View File

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

View File

@@ -1,12 +1,10 @@
<AI_AGENT_ARCHITECT_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента-Архитектора'**. Он описывает философию, процедуры и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли, используя высокоуровневый `gitea-client.zsh` для взаимодействия с Gitea.</PURPOSE>
<VERSION>7.0</VERSION>
<VERSION>8.0</VERSION>
<!-- 1. Указываем, что используем каталог метрик -->
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
<!-- 2. Декларируем, какие именно метрики собирает эта роль -->
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
<COLLECTS group_id="core_metrics"/>
@@ -94,7 +92,7 @@
<WORKFLOW_STEP id="7" name="Log_Execution_Metrics">
<ACTION>Исполняющая среда ДОЛЖНА собрать все метрики, задекларированные в METRICS_TO_COLLECT.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в систему логирования.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyMetricsSink.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
@@ -118,4 +116,4 @@
</STRUCTURE>
</RESPONSE_FORMAT>
</AI_AGENT_ARCHITECT_PROTOCOL>
</AI_AGENT_ARCHITECT_PROTOCOL>

View File

@@ -0,0 +1,29 @@
<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"/>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>Переопределить в дочерней роли.</SPECIALIZATION>
<CORE_GOAL>Переопределить в дочерней роли.</CORE_GOAL>
</ROLE_DEFINITION>
<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,9 +1,9 @@
<AI_AGENT_DOCUMENTATION_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Документации'**. Он описывает философию, процедуры инициализации и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли. Главная задача — синхронизация `PROJECT_MANIFEST.xml` с текущим состоянием кодовой базы.</PURPOSE>
<VERSION>3.0</VERSION>
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
<VERSION>4.0</VERSION>
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
@@ -111,7 +111,7 @@
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Log_Execution_Metrics">
<ACTION>Исполняющая среда ДОЛЖНА собрать все метрики, задекларированные в METRICS_TO_COLLECT.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в систему логирования.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyMetricsSink.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_DOCUMENTATION_PROTOCOL>

View File

@@ -1,14 +1,9 @@
<!--
Роль Инженера.
Основная задача: преобразовать бизнес-намерение (WorkOrder) в полностью реализованный и семантически богатый код.
Эта версия промта использует абстрактные каналы для коммуникаций.
-->
<AI_AGENT_ROLE_PROTOCOL name="Engineer">
<EXTENDS from="base_role.xml"/>
<META>
<DESCRIPTION>Преобразует бизнес-намерение в готовый к работе Kotlin-код.</DESCRIPTION>
<VERSION>2.0</VERSION>
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
<VERSION>3.0</VERSION>
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
@@ -21,12 +16,16 @@
<REQUIRES_CHANNEL type="TaskSource" as="MyTaskInbox"/>
<REQUIRES_CHANNEL type="LogSink" as="MyLogger"/>
<!-- Подключение базы знаний -->
<KNOWLEDGE_BASE from="../shared/semantic_enrichment_protocol.xml"/>
<DEPENDS_ON from="../protocols/semantic_enrichment_protocol.xml"/>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный разработчик. Моя задача — преобразовать бизнес-намерение (WorkOrder) в полностью реализованный и семантически богатый код на языке Kotlin, следуя всем протоколам и базам знаний.</SPECIALIZATION>
<CORE_GOAL>Создать готовый к работе, семантически размеченный и соответствующий всем контрактам код, который реализует поставленную задачу.</CORE_GOAL>
</ROLE_DEFINITION>
<!-- Основной цикл работы агента -->
<ACTION>
<MASTER_WORKFLOW name="Engineer_Workflow">
<!-- 1. Получить задачу из абстрактного источника -->
<LET name="WorkOrder" value="CALL MyTaskInbox.GetNextPendingTask()"/>
@@ -45,9 +44,9 @@
<!-- 4. Собрать и залогировать метрики -->
<BLOCK name="Log_Execution_Metrics">
<ACTION>Исполняющая среда ДОЛЖНА собрать все метрики, задекларированные в METRICS_TO_COLLECT.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyLogger.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyMetricsSink.</ACTION>
</BLOCK>
</ACTION>
</MASTER_WORKFLOW>
<!-- Воркфлоу остается здесь, т.к. это основная логика роли -->
<SUB_WORKFLOW name="EXECUTE_INTENT_WORKFLOW">
@@ -55,4 +54,4 @@
<!-- ... шаги E1-E5 из вашего файла GEMINI.md ... -->
</SUB_WORKFLOW>
</AI_AGENT_ROLE_PROTOCOL>
</AI_AGENT_ROLE_PROTOCOL>

67
agent_promts/roles/qa.xml Normal file
View File

@@ -0,0 +1,67 @@
<AI_AGENT_ROLE_PROTOCOL name="QA_Tester">
<EXTENDS from="base_role.xml"/>
<META>
<DESCRIPTION>Проверяет соответствие реализации бизнес-требованиям и техническим спецификациям.</DESCRIPTION>
<VERSION>1.0</VERSION>
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает метрики для анализа качества и полноты тестирования.</DESCRIPTION>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="qa_specific"/>
</METRICS_TO_COLLECT>
<!-- Декларация потребностей в каналах -->
<REQUIRES_CHANNEL type="TaskSource" as="MyTaskInbox"/>
<REQUIRES_CHANNEL type="LogSink" as="MyLogger"/>
<DEPENDS_ON from="../protocols/gitea_protocol.xml"/>
<DEPENDS_ON from="../protocols/semantic_enrichment_protocol.xml"/>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный QA-инженер. Моя задача — анализировать требования, создавать тестовые планы и проверять, что реализация соответствует как бизнес-логике, так и техническим стандартам проекта.</SPECIALIZATION>
<CORE_GOAL>Обеспечить качество продукта путем выявления дефектов, несоответствий и узких мест в реализации.</CORE_GOAL>
</ROLE_DEFINITION>
<MASTER_WORKFLOW name="QA_Workflow">
<!-- 1. Получить задачу из абстрактного источника -->
<LET name="WorkOrder" value="CALL MyTaskInbox.GetNextPendingTask()"/>
<IF condition="WorkOrder IS NULL">
<SEND message="No pending tasks found for QA." to="MyLogger"/>
<TERMINATE/>
</IF>
<!-- 2. Выполнить основной цикл тестирования -->
<LET name="TestResult" value="EXECUTE_QA_WORKFLOW(WorkOrder)"/>
<!-- 3. Отправить отчет о тестировании в логгер -->
<SEND message="TestResult" to="MyLogger"/>
<!-- 4. Собрать и залогировать метрики -->
<BLOCK name="Log_QA_Metrics">
<ACTION>Исполняющая среда ДОЛЖНА собрать все метрики, задекларированные в METRICS_TO_COLLECT.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyMetricsSink.</ACTION>
</BLOCK>
</MASTER_WORKFLOW>
<SUB_WORKFLOW name="EXECUTE_QA_WORKFLOW">
<INPUT>WorkOrder</INPUT>
<STEPS>
<STEP name="Analyze_Requirements">
<ACTION>Проанализировать WorkOrder и связанные с ним артефакты (например, тикеты в Gitea, спецификации).</ACTION>
</STEP>
<STEP name="Create_Test_Plan">
<ACTION>На основе анализа создать детальный план тестирования, покрывающий позитивные и негативные сценарии.</ACTION>
</STEP>
<STEP name="Execute_Tests">
<ACTION>Выполнить тесты. Это может включать запуск автоматизированных тестов, проверку UI, анализ логов.</ACTION>
</STEP>
<STEP name="Report_Findings">
<ACTION>Сформировать отчет о результатах тестирования. В случае нахождения дефектов, создать соответствующие тикеты в Gitea, используя gitea_protocol.</ACTION>
</STEP>
</STEPS>
</SUB_WORKFLOW>
</AI_AGENT_ROLE_PROTOCOL>

View File

@@ -1,9 +1,9 @@
<AI_AGENT_SEMANTIC_LINTER_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантической Разметки'**. Он описывает философию, процедуры инициализации и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли. Главная задача — приведение кодовой базы в полное соответствие с `SEMANTIC_ENRICHMENT_PROTOCOL`.</PURPOSE>
<VERSION>3.0</VERSION>
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
<VERSION>4.0</VERSION>
<METRICS_TO_COLLECT>
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
@@ -144,7 +144,7 @@
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Log_Execution_Metrics">
<ACTION>Исполняющая среда ДОЛЖНА собрать все метрики, задекларированные в METRICS_TO_COLLECT.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в систему логирования.</ACTION>
<ACTION>Собранные метрики ДОЛЖНЫ быть отправлены в MyMetricsSink.</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_SEMANTIC_LINTER_PROTOCOL>

View File

@@ -37,4 +37,11 @@
<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>
</METRICS_CATALOG>

View File

@@ -20,6 +20,7 @@ import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
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.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
@@ -110,6 +111,23 @@ fun NavGraph(
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) {
SearchScreen(
currentRoute = currentRoute,

View File

@@ -77,6 +77,21 @@ sealed class Screen(val route: String) {
data object LabelsList : Screen("labels_list_screen")
// [END_ENTITY: Object('LabelsList')]
// [ENTITY: Object('LabelEdit')]
data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Создает маршрут для экрана редактирования метки с указанным ID.
* @param labelId ID метки для редактирования. Null, если создается новая метка.
* @return Строку полного маршрута.
*/
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')]
data object LocationsList : Screen("locations_list_screen")
// [END_ENTITY: Object('LocationsList')]

View File

@@ -0,0 +1,73 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] ColorPicker.kt
// [SEMANTICS] ui, component, color_selection
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 Компонент для выбора цвета.
* @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF").
* @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета.
* @param modifier Модификатор для настройки внешнего вида.
*/
@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(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

@@ -0,0 +1,35 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] LoadingOverlay.kt
// [SEMANTICS] 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 Полноэкранный оверлей с индикатором загрузки.
*/
@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

@@ -0,0 +1,113 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditScreen.kt
// [SEMANTICS] ui, screen, label, edit
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-функция для экрана "Редактирование метки".
* @param labelId ID метки для редактирования или null для создания новой.
* @param onBack Навигация назад.
* @param onLabelSaved Действие после сохранения метки.
*/
@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))
ColorPicker(
selectedColor = uiState.color,
onColorSelected = viewModel::onColorChange,
modifier = Modifier.fillMaxWidth()
)
}
if (uiState.isLoading) {
LoadingOverlay()
}
}
}
// [END_ENTITY: Function('LabelEditScreen')]
// [END_FILE_LabelEditScreen.kt]

View File

@@ -0,0 +1,115 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditViewModel.kt
// [SEMANTICS] ui, viewmodel, label_management
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 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 {
if (labelId == null) {
// Create new label
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
createLabelUseCase(newLabel)
} else {
// Update existing label
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
updateLabelUseCase(labelId, updatedLabel)
}
uiState = uiState.copy(isSaved = true)
} catch (e: Exception) {
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 {
val label = getLabelDetailsUseCase(id)
uiState = uiState.copy(
name = label.name,
color = label.color,
isLoading = false
)
} catch (e: Exception) {
uiState = uiState.copy(error = e.message, isLoading = false)
}
}
}
}
// [ENTITY: DataClass('LabelEditUiState')]
/**
* @summary Состояние UI для экрана редактирования метки.
*/
data class LabelEditUiState(
val name: String = "",
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

@@ -82,8 +82,8 @@ fun LabelsListScreen(
},
floatingActionButton = {
FloatingActionButton(onClick = {
Timber.i("[INFO][ACTION][show_create_dialog] FAB clicked: Initiate create new label flow.")
viewModel.onShowCreateDialog()
Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.")
navController.navigate(Screen.LabelEdit.createRoute())
}) {
Icon(
imageVector = Icons.Default.Add,
@@ -93,16 +93,6 @@ fun LabelsListScreen(
}
) { paddingValues ->
val currentState = uiState
if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) {
CreateLabelDialog(
onConfirm = { labelName ->
viewModel.createLabel(labelName)
},
onDismiss = {
viewModel.onDismissCreateDialog()
}
)
}
Box(
modifier = Modifier
@@ -119,14 +109,13 @@ fun LabelsListScreen(
}
is LabelsListUiState.Success -> {
if (currentState.labels.isEmpty()) {
Text(text = stringResource(id = R.string.labels_list_empty))
Text(text = stringResource(id = R.string.no_labels_found))
} else {
LabelsList(
labels = currentState.labels,
onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.")
val route = Screen.InventoryList.withFilter("label", label.id)
navController.navigate(route)
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
navController.navigate(Screen.LabelEdit.createRoute(label.id))
}
)
}
@@ -191,46 +180,4 @@ private fun 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]

View File

@@ -99,4 +99,17 @@
<string name="dialog_button_create">Создать</string>
<string name="dialog_button_cancel">Отмена</string>
<!-- Label Edit Screen -->
<string name="label_edit_title_create">Создать метку</string>
<string name="label_edit_title_edit">Редактировать метку</string>
<string name="label_name_edit">Название метки</string>
<!-- Common Actions -->
<string name="back">Назад</string>
<string name="save">Сохранить</string>
<!-- Common Actions -->
<!-- Color Picker -->
<string name="label_color">Цвет</string>
<string name="label_hex_color">HEX-код цвета</string>
</resources>

View File

@@ -96,6 +96,14 @@ class ItemRepositoryImpl @Inject constructor(
}
// [END_ENTITY: Function('getAllLabels')]
// [ENTITY: Function('getLabelDetails')]
// [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')]
override suspend fun getLabelDetails(labelId: String): LabelOut {
val resultDto = apiService.getLabels().firstOrNull { it.id == labelId }
return resultDto?.toDomain() ?: throw NoSuchElementException("Label with ID $labelId not found.")
}
// [END_ENTITY: Function('getLabelDetails')]
// [ENTITY: Function('createLabel')]
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {

View File

@@ -92,6 +92,17 @@ interface ItemRepository {
suspend fun getAllLabels(): List<LabelOut>
// [END_ENTITY: Function('getAllLabels')]
// [ENTITY: Function('getLabelDetails')]
// [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')]
/**
* @summary Получает детальную информацию о метке.
* @param labelId ID метки.
* @return Детальная информация о метке.
*/
suspend fun getLabelDetails(labelId: String): LabelOut
// [END_ENTITY: Function('getLabelDetails')]
// [ENTITY: Function('createLabel')]
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
/**

View File

@@ -0,0 +1,35 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] GetLabelDetailsUseCase.kt
// [SEMANTICS] business_logic, use_case, label_retrieval
package com.homebox.lens.domain.usecase
// [IMPORTS]
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
/**
* @summary Получает детальную информацию о метке по ее ID.
* @param itemRepository Репозиторий для работы с данными о метках.
*/
class GetLabelDetailsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* @summary Выполняет получение детальной информации о метке.
* @param labelId ID запрашиваемой метки.
* @return Детальная информация о метке [LabelOut].
* @throws IllegalArgumentException если `labelId` пустой.
* @throws NoSuchElementException если метка с указанным ID не найдена.
*/
suspend operator fun invoke(labelId: String): LabelOut {
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
return itemRepository.getLabelDetails(labelId)
}
}
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
// [END_FILE_GetLabelDetailsUseCase.kt]

View File

@@ -45,7 +45,7 @@
# [RELATION: 'EntryPoint'('main_dispatch')] -> [CALLS] -> ['Function'('return_to_dev')]
# [END_SEMANTICS]
set -x
# [DEPENDENCIES]
# Gitea Client Script
@@ -122,9 +122,12 @@ function api_request() {
response_body=$(<"$body_file")
rm -f "$body_file" # Очистка после использования
echo "DEBUG: HTTP Code: $http_code" >&2
echo "DEBUG: Response Body: $response_body" >&2
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
if [[ -z "$response_body" ]]; then
echo "{\"http_status\": $http_code, \"body\": \"empty\"}"
echo "{""http_status"": $http_code, ""body"": ""empty""}"
else
echo "$response_body"
fi

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<ASSURANCE_REPORT>
<METADATA>
<REPORT_ID>20250906_label_management_feature_qa_report</REPORT_ID>
<FEATURE_NAME>Label Management Feature</FEATURE_NAME>
<DESCRIPTION>
This report details the implementation of the Label Management Feature,
including creation, viewing, and editing of labels.
It provides verification steps for the QA agent.
</DESCRIPTION>
<DATE>2025-09-06</DATE>
<STATUS>Ready for QA</STATUS>
</METADATA>
<AFFECTED_COMPONENTS>
<COMPONENT path="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt"/>
<COMPONENT path="data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt"/>
<COMPONENT path="domain/src/main/java/com/homebox/lens/domain/usecase/GetLabelDetailsUseCase.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt"/>
<COMPONENT path="app/src/main/res/values/strings.xml"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/navigation/Screen.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt"/>
<COMPONENT path="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt"/>
</AFFECTED_COMPONENTS>
<VERIFICATION_STEPS>
<PREREQUISITES>
<STEP>Ensure the application is built and running on a device/emulator.</STEP>
</PREREQUISITES>
<SCENARIO id="SCN_001" name="Create New Label">
<STEP>Navigate to the Labels List screen.</STEP>
<STEP>Tap the "Add" (plus) Floating Action Button.</STEP>
<STEP>Verify that the "Create Label" screen appears.</STEP>
<STEP>Enter a label name (e.g., "My New Label") and select a color using the color picker.</STEP>
<STEP>Tap the "Save" button (check icon in top app bar).</STEP>
<STEP>Verify that the new label appears in the Labels List.</STEP>
</SCENARIO>
<SCENARIO id="SCN_002" name="Edit Existing Label">
<STEP>Navigate to the Labels List screen.</STEP>
<STEP>Tap on an existing label from the list.</STEP>
<STEP>Verify that the "Edit Label" screen appears with the label's current name and color pre-filled.</STEP>
<STEP>Modify the label name (e.g., "Updated Label") and/or color.</STEP>
<STEP>Tap the "Save" button (check icon in top app bar).</STEP>
<STEP>Verify that the label's changes are reflected in the Labels List.</STEP>
</SCENARIO>
<SCENARIO id="SCN_003" name="Validation - Empty Label Name">
<STEP>Navigate to the "Create Label" screen (via FAB).</STEP>
<STEP>Leave the label name field empty.</STEP>
<STEP>Tap the "Save" button.</STEP>
<STEP>Verify that an error message "Label name cannot be empty." is displayed below the name input field.</STEP>
<STEP>Verify that the label is NOT saved and the screen remains open.</STEP>
</SCENARIO>
<SCENARIO id="SCN_004" name="Navigation">
<STEP>From the Labels List screen, navigate to the "Create Label" screen.</STEP>
<STEP>Tap the "Back" arrow in the top app bar.</STEP>
<STEP>Verify that the app navigates back to the Labels List screen.</STEP>
<STEP>From the Labels List screen, tap on an existing label to go to the "Edit Label" screen.</STEP>
<STEP>Tap the "Back" arrow in the top app bar.</STEP>
<STEP>Verify that the app navigates back to the Labels List screen.</STEP>
</SCENARIO>
</VERIFICATION_STEPS>
</ASSURANCE_REPORT>

View File

@@ -1,35 +1,71 @@
<WORK_ORDERS>
<WORK_ORDER id="1" title="Implement Inventory List Screen">
<INTENT_SPECIFICATION>
Implement the UI for the inventory list screen in `InventoryListScreen.kt` to display a list of items using Jetpack Compose.
Implement the `InventoryListViewModel.kt` to fetch a paginated list of items from the `ItemRepository` and expose it to the UI.
The screen should show a loading indicator while data is being fetched and handle empty or error states.
</INTENT_SPECIFICATION>
<TARGET_FILES>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt</FILE>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt</FILE>
</TARGET_FILES>
</WORK_ORDER>
<WORK_ORDER id="2" title="Implement Item Details Screen">
<INTENT_SPECIFICATION>
Implement the UI for the item details screen in `ItemDetailsScreen.kt`. It should display all the information about a specific item.
Implement the `ItemDetailsViewModel.kt` to fetch the details of a single item from the `ItemRepository` using its ID.
The screen should handle cases where the item is not found.
</INTENT_SPECIFICATION>
<TARGET_FILES>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt</FILE>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt</FILE>
</TARGET_FILES>
</WORK_ORDER>
<WORK_ORDER id="3" title="Implement Search Screen">
<INTENT_SPECIFICATION>
Implement the UI for the search screen in `SearchScreen.kt`. It should contain a search bar and a list to display search results.
Implement the `SearchViewModel.kt` to take a search query, call the `SearchItemsUseCase`, and expose the results to the UI.
The search should be triggered as the user types, with debouncing to avoid excessive API calls.
</INTENT_SPECIFICATION>
<TARGET_FILES>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt</FILE>
<FILE>app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt</FILE>
</TARGET_FILES>
</WORK_ORDER>
</WORK_ORDERS>
<?xml version="1.0" encoding="UTF-8"?>
<WORK_ORDER>
<METADATA>
<WORK_ORDER_ID>20250906_100000</WORK_ORDER_ID>
<TITLE>[ARCHITECT -> DEV] Implement Label Management Feature</TITLE>
<DESCRIPTION>
This work order is to implement the full lifecycle of label management,
including creating, viewing, editing, and deleting labels.
This involves creating a new screen for editing labels, a view model to handle the logic,
and integrating it with the existing label list screen.
</DESCRIPTION>
<STATUS>Completed</STATUS>
<ASSIGNEE>agent-developer</ASSIGNEE>
<LABELS>
<LABEL>type::development</LABEL>
<LABEL>feature::label-management</LABEL>
</LABELS>
</METADATA>
<TASKS>
<TASK id="task_1" name="Create LabelEditViewModel">
<DESCRIPTION>
Create a new ViewModel `LabelEditViewModel.kt` in `app/src/main/java/com/homebox/lens/ui/screen/labeledit/`.
This ViewModel should handle the business logic for creating and updating a label.
It should use `GetLabelDetailsUseCase`, `CreateLabelUseCase`, and `UpdateLabelUseCase`.
</DESCRIPTION>
<CHECKLIST>
<ITEM>Create `app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt`</ITEM>
<ITEM>Inject `GetLabelDetailsUseCase`, `CreateLabelUseCase`, `UpdateLabelUseCase`.</ITEM>
<ITEM>Implement state management for the label editing screen.</ITEM>
<ITEM>Implement methods to create and update a label.</ITEM>
</CHECKLIST>
</TASK>
<TASK id="task_2" name="Create LabelEditScreen">
<DESCRIPTION>
Create a new Jetpack Compose screen `LabelEditScreen.kt` in `app/src/main/java/com/homebox/lens/ui/screen/labeledit/`.
This screen will be used for both creating a new label and editing an existing one.
The UI should be similar to the `LocationEditScreen`.
</DESCRIPTION>
<CHECKLIST>
<ITEM>Create `app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt`</ITEM>
<ITEM>Implement the UI for creating/editing a label (e.g., a text field for the name and a color picker).</ITEM>
<ITEM>Connect the screen to `LabelEditViewModel`.</ITEM>
</CHECKLIST>
</TASK>
<TASK id="task_3" name="Update Navigation">
<DESCRIPTION>
Update the navigation graph to include the new `LabelEditScreen`.
The `LabelsListScreen` should navigate to `LabelEditScreen` when the user wants to create or edit a label.
</DESCRIPTION>
<CHECKLIST>
<ITEM>Add a route for `LabelEditScreen` in `Screen.kt`.</ITEM>
<ITEM>Add the new screen to the `NavGraph.kt`.</ITEM>
<ITEM>Implement navigation from `LabelsListScreen` to `LabelEditScreen`.</ITEM>
</CHECKLIST>
</TASK>
<TASK id="task_4" name="Create GetLabelDetailsUseCase">
<DESCRIPTION>
Create a new UseCase `GetLabelDetailsUseCase.kt` in `domain/src/main/java/com/homebox/lens/domain/usecase/`.
This UseCase will be responsible for getting the details of a single label.
</DESCRIPTION>
<CHECKLIST>
<ITEM>Create `domain/src/main/java/com/homebox/lens/domain/usecase/GetLabelDetailsUseCase.kt`</ITEM>
<ITEM>Implement the logic to get label details from the `ItemRepository`.</ITEM>
</CHECKLIST>
</TASK>
</TASKS>
</WORK_ORDER>