qa roles
This commit is contained in:
@@ -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>
|
||||
|
||||
17
agent_promts/implementations/xml_file_metrics_sink.xml
Normal file
17
agent_promts/implementations/xml_file_metrics_sink.xml
Normal 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>
|
||||
7
agent_promts/interfaces/metrics_sink_interface.xml
Normal file
7
agent_promts/interfaces/metrics_sink_interface.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<!--
|
||||
Абстрактный контракт для любого приемника метрик.
|
||||
Он гарантирует, что у любого приемника будет метод Send для записи метрик.
|
||||
-->
|
||||
<INTERFACE name="MetricsSink">
|
||||
<METHOD name="Send" accepts="MetricsBundle"/>
|
||||
</INTERFACE>
|
||||
85
agent_promts/protocols/gitea_protocol.xml
Normal file
85
agent_promts/protocols/gitea_protocol.xml
Normal 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>
|
||||
12
agent_promts/protocols/semantic_enrichment_protocol.xml
Normal file
12
agent_promts/protocols/semantic_enrichment_protocol.xml
Normal 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>
|
||||
@@ -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>
|
||||
29
agent_promts/roles/base_role.xml
Normal file
29
agent_promts/roles/base_role.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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
67
agent_promts/roles/qa.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')]
|
||||
/**
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user