+md
This commit is contained in:
@@ -64,18 +64,32 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
**Prerequisites:** `research.md` complete
|
||||
|
||||
0. **Validate Design against UX Reference**:
|
||||
- Check if the proposed architecture supports the latency, interactivity, and flow defined in `ux_reference.md`.
|
||||
- **Linkage**: Ensure key UI states from `ux_reference.md` map to Component Contracts (`@UX_STATE`).
|
||||
- **CRITICAL**: If the technical plan compromises the UX (e.g. "We can't do real-time validation"), you **MUST STOP** and warn the user.
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
- Entity name, fields, relationships, validation rules.
|
||||
|
||||
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
|
||||
- Identify what interfaces the project exposes to users or other systems
|
||||
- Document the contract format appropriate for the project type
|
||||
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
|
||||
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||
2. **Design & Verify Contracts (Semantic Protocol)**:
|
||||
- **Drafting**: Define [DEF] Headers and Contracts for all new modules based on `.ai/standards/semantics.md`.
|
||||
- **TIER Classification**: Explicitly assign `@TIER: [CRITICAL|STANDARD|TRIVIAL]` to each module.
|
||||
- **CRITICAL Requirements**: For all CRITICAL modules, define full `@PRE`, `@POST`, and (if UI) `@UX_STATE` contracts.
|
||||
- **Self-Review**:
|
||||
- *Completeness*: Do `@PRE`/`@POST` cover edge cases identified in Research?
|
||||
- *Connectivity*: Do `@RELATION` tags form a coherent graph?
|
||||
- *Compliance*: Does syntax match `[DEF:id:Type]` exactly?
|
||||
- **Output**: Write verified contracts to `contracts/modules.md`.
|
||||
|
||||
3. **Agent context update**:
|
||||
3. **Simulate Contract Usage**:
|
||||
- Trace one key user scenario through the defined contracts to ensure data flow continuity.
|
||||
- If a contract interface mismatch is found, fix it immediately.
|
||||
|
||||
4. **Generate API contracts**:
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/` for backend-frontend sync.
|
||||
|
||||
5. **Agent context update**:
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh kilocode`
|
||||
- These scripts detect which AI agent is in use
|
||||
- Update the appropriate agent-specific context file
|
||||
|
||||
@@ -392,11 +392,11 @@ def test_status_query_without_task_id_returns_latest_user_task():
|
||||
|
||||
|
||||
# [/DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
|
||||
# [DEF:test_llm_validation_missing_dashboard_returns_needs_clarification:Function]
|
||||
# @PURPOSE: LLM validation command without resolvable dashboard id must request clarification instead of generic failure.
|
||||
# @PRE: Command intent resolves to run_llm_validation but dashboard id cannot be inferred.
|
||||
# @POST: Assistant response state is needs_clarification with guidance text.
|
||||
def test_llm_validation_missing_dashboard_returns_needs_clarification():
|
||||
# [DEF:test_llm_validation_with_dashboard_ref_requires_confirmation:Function]
|
||||
# @PURPOSE: LLM validation with dashboard_ref should now require confirmation before dispatch.
|
||||
# @PRE: User sends natural-language validation request with dashboard name (not numeric id).
|
||||
# @POST: Response state is needs_confirmation since all state-changing operations are now gated.
|
||||
def test_llm_validation_with_dashboard_ref_requires_confirmation():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
@@ -410,8 +410,11 @@ def test_llm_validation_missing_dashboard_returns_needs_clarification():
|
||||
)
|
||||
)
|
||||
|
||||
assert response.state == "needs_clarification"
|
||||
assert "Укажите" in response.text or "Missing dashboard_id" in response.text
|
||||
assert response.state == "needs_confirmation"
|
||||
assert response.confirmation_id is not None
|
||||
action_types = {a.type for a in response.actions}
|
||||
assert "confirm" in action_types
|
||||
assert "cancel" in action_types
|
||||
|
||||
|
||||
# [/DEF:test_llm_validation_missing_dashboard_returns_needs_clarification:Function]
|
||||
@@ -555,4 +558,71 @@ def test_list_conversations_archived_only_filters_active():
|
||||
|
||||
|
||||
# [/DEF:test_list_conversations_archived_only_filters_active:Function]
|
||||
|
||||
|
||||
# [DEF:test_guarded_operation_always_requires_confirmation:Function]
|
||||
# @PURPOSE: Non-dangerous (guarded) commands must still require confirmation before execution.
|
||||
# @PRE: Admin user sends a backup command that was previously auto-executed.
|
||||
# @POST: Response state is needs_confirmation with confirm and cancel actions.
|
||||
def test_guarded_operation_always_requires_confirmation():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="сделай бэкап окружения dev"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "needs_confirmation"
|
||||
assert response.confirmation_id is not None
|
||||
action_types = {a.type for a in response.actions}
|
||||
assert "confirm" in action_types
|
||||
assert "cancel" in action_types
|
||||
assert "Выполнить" in response.text or "Подтвердите" in response.text
|
||||
|
||||
|
||||
# [/DEF:test_guarded_operation_always_requires_confirmation:Function]
|
||||
|
||||
|
||||
# [DEF:test_guarded_operation_confirm_roundtrip:Function]
|
||||
# @PURPOSE: Guarded operation must execute successfully after explicit confirmation.
|
||||
# @PRE: Admin user sends a non-dangerous migration command (dev → dev).
|
||||
# @POST: After confirmation, response transitions to started/success with task_id.
|
||||
def test_guarded_operation_confirm_roundtrip():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
first = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на dev для дашборда 5"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert first.state == "needs_confirmation"
|
||||
assert first.confirmation_id
|
||||
|
||||
second = _run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=first.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert second.state == "started"
|
||||
assert second.task_id is not None
|
||||
|
||||
|
||||
# [/DEF:test_guarded_operation_confirm_roundtrip:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
|
||||
@@ -918,6 +918,46 @@ def _coerce_intent_entities(intent: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# [/DEF:_coerce_intent_entities:Function]
|
||||
|
||||
|
||||
# Operations that are read-only and do not require confirmation.
|
||||
_SAFE_OPS = {"show_capabilities", "get_task_status"}
|
||||
|
||||
|
||||
# [DEF:_confirmation_summary:Function]
|
||||
# @PURPOSE: Build human-readable confirmation prompt for an intent before execution.
|
||||
# @PRE: intent contains operation and entities fields.
|
||||
# @POST: Returns descriptive Russian-language text ending with confirmation prompt.
|
||||
def _confirmation_summary(intent: Dict[str, Any]) -> str:
|
||||
operation = intent.get("operation", "")
|
||||
entities = intent.get("entities", {})
|
||||
descriptions: Dict[str, str] = {
|
||||
"create_branch": "создание ветки{branch} для дашборда{dashboard}",
|
||||
"commit_changes": "коммит изменений для дашборда{dashboard}",
|
||||
"deploy_dashboard": "деплой дашборда{dashboard} в окружение{env}",
|
||||
"execute_migration": "миграция дашборда{dashboard} с{src} на{tgt}",
|
||||
"run_backup": "бэкап окружения{env}{dashboard}",
|
||||
"run_llm_validation": "LLM-валидация дашборда{dashboard}{env}",
|
||||
"run_llm_documentation": "генерация документации для датасета{dataset}{env}",
|
||||
}
|
||||
template = descriptions.get(operation)
|
||||
if not template:
|
||||
return "Подтвердите выполнение операции или отмените."
|
||||
|
||||
def _label(value: Any, prefix: str = " ") -> str:
|
||||
return f"{prefix}{value}" if value else ""
|
||||
|
||||
dashboard = entities.get("dashboard_id") or entities.get("dashboard_ref")
|
||||
text = template.format(
|
||||
branch=_label(entities.get("branch_name")),
|
||||
dashboard=_label(dashboard),
|
||||
env=_label(entities.get("environment") or entities.get("target_env")),
|
||||
src=_label(entities.get("source_env")),
|
||||
tgt=_label(entities.get("target_env")),
|
||||
dataset=_label(entities.get("dataset_id")),
|
||||
)
|
||||
return f"Выполнить: {text}. Подтвердите или отмените."
|
||||
# [/DEF:_confirmation_summary:Function]
|
||||
|
||||
|
||||
# [DEF:_clarification_text_for_intent:Function]
|
||||
# @PURPOSE: Convert technical missing-parameter errors into user-facing clarification prompts.
|
||||
# @PRE: state was classified as needs_clarification for current intent/error combination.
|
||||
@@ -1328,14 +1368,7 @@ async def send_message(
|
||||
except Exception as exc:
|
||||
logger.warning(f"[assistant.planner][fallback] Planner error: {exc}")
|
||||
if not intent:
|
||||
intent = {
|
||||
"domain": "unknown",
|
||||
"operation": "clarify",
|
||||
"entities": {},
|
||||
"confidence": 0.0,
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
intent = _parse_command(request.message, config_manager)
|
||||
confidence = float(intent.get("confidence", 0.0))
|
||||
|
||||
if intent.get("domain") == "unknown" or confidence < 0.6:
|
||||
@@ -1358,7 +1391,8 @@ async def send_message(
|
||||
try:
|
||||
_authorize_intent(intent, current_user)
|
||||
|
||||
if intent.get("requires_confirmation"):
|
||||
operation = intent.get("operation")
|
||||
if operation not in _SAFE_OPS:
|
||||
confirmation_id = str(uuid.uuid4())
|
||||
confirm = ConfirmationRecord(
|
||||
id=confirmation_id,
|
||||
@@ -1371,7 +1405,7 @@ async def send_message(
|
||||
)
|
||||
CONFIRMATIONS[confirmation_id] = confirm
|
||||
_persist_confirmation(db, confirm)
|
||||
text = "Операция рискованная. Подтвердите выполнение или отмените."
|
||||
text = _confirmation_summary(intent)
|
||||
_append_history(
|
||||
user_id,
|
||||
conversation_id,
|
||||
@@ -1388,7 +1422,13 @@ async def send_message(
|
||||
text,
|
||||
state="needs_confirmation",
|
||||
confirmation_id=confirmation_id,
|
||||
metadata={"intent": intent},
|
||||
metadata={
|
||||
"intent": intent,
|
||||
"actions": [
|
||||
{"type": "confirm", "label": "✅ Подтвердить", "target": confirmation_id},
|
||||
{"type": "cancel", "label": "❌ Отменить", "target": confirmation_id},
|
||||
],
|
||||
},
|
||||
)
|
||||
audit_payload = {
|
||||
"decision": "needs_confirmation",
|
||||
@@ -1406,12 +1446,13 @@ async def send_message(
|
||||
intent=intent,
|
||||
confirmation_id=confirmation_id,
|
||||
actions=[
|
||||
AssistantAction(type="confirm", label="Confirm", target=confirmation_id),
|
||||
AssistantAction(type="cancel", label="Cancel", target=confirmation_id),
|
||||
AssistantAction(type="confirm", label="✅ Подтвердить", target=confirmation_id),
|
||||
AssistantAction(type="cancel", label="❌ Отменить", target=confirmation_id),
|
||||
],
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
# Read-only operations execute immediately
|
||||
text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)
|
||||
state = "started" if task_id else "success"
|
||||
_append_history(user_id, conversation_id, "assistant", text, state=state, task_id=task_id)
|
||||
|
||||
@@ -88,6 +88,20 @@ class DashboardDetailResponse(BaseModel):
|
||||
dataset_count: int
|
||||
# [/DEF:DashboardDetailResponse:DataClass]
|
||||
|
||||
# [DEF:DatabaseMapping:DataClass]
|
||||
class DatabaseMapping(BaseModel):
|
||||
source_db: str
|
||||
target_db: str
|
||||
source_db_uuid: Optional[str] = None
|
||||
target_db_uuid: Optional[str] = None
|
||||
confidence: float
|
||||
# [/DEF:DatabaseMapping:DataClass]
|
||||
|
||||
# [DEF:DatabaseMappingsResponse:DataClass]
|
||||
class DatabaseMappingsResponse(BaseModel):
|
||||
mappings: List[DatabaseMapping]
|
||||
# [/DEF:DatabaseMappingsResponse:DataClass]
|
||||
|
||||
# [DEF:get_dashboards:Function]
|
||||
# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status
|
||||
# @PRE: env_id must be a valid environment ID
|
||||
@@ -168,12 +182,54 @@ async def get_dashboards(
|
||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboards: {str(e)}")
|
||||
# [/DEF:get_dashboards:Function]
|
||||
|
||||
# [DEF:get_database_mappings:Function]
|
||||
# @PURPOSE: Get database mapping suggestions between source and target environments
|
||||
# @PRE: User has permission plugin:migration:read
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @POST: Returns list of suggested database mappings with confidence scores
|
||||
# @PARAM: source_env_id (str) - Source environment ID
|
||||
# @PARAM: target_env_id (str) - Target environment ID
|
||||
# @RETURN: DatabaseMappingsResponse - List of suggested mappings
|
||||
# @RELATION: CALLS -> MappingService.get_suggestions
|
||||
@router.get("/db-mappings", response_model=DatabaseMappingsResponse)
|
||||
async def get_database_mappings(
|
||||
source_env_id: str,
|
||||
target_env_id: str,
|
||||
mapping_service=Depends(get_mapping_service),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_database_mappings", f"source={source_env_id}, target={target_env_id}"):
|
||||
try:
|
||||
# Get mapping suggestions using MappingService
|
||||
suggestions = await mapping_service.get_suggestions(source_env_id, target_env_id)
|
||||
|
||||
# Format suggestions as DatabaseMapping objects
|
||||
mappings = [
|
||||
DatabaseMapping(
|
||||
source_db=s.get('source_db', ''),
|
||||
target_db=s.get('target_db', ''),
|
||||
source_db_uuid=s.get('source_db_uuid'),
|
||||
target_db_uuid=s.get('target_db_uuid'),
|
||||
confidence=s.get('confidence', 0.0)
|
||||
)
|
||||
for s in suggestions
|
||||
]
|
||||
|
||||
logger.info(f"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions")
|
||||
|
||||
return DatabaseMappingsResponse(mappings=mappings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}")
|
||||
raise HTTPException(status_code=503, detail=f"Failed to get database mappings: {str(e)}")
|
||||
# [/DEF:get_database_mappings:Function]
|
||||
|
||||
# [DEF:get_dashboard_detail:Function]
|
||||
# @PURPOSE: Fetch detailed dashboard info with related charts and datasets
|
||||
# @PRE: env_id must be valid and dashboard_id must exist
|
||||
# @POST: Returns dashboard detail payload for overview page
|
||||
# @RELATION: CALLS -> SupersetClient.get_dashboard_detail
|
||||
@router.get("/{dashboard_id}", response_model=DashboardDetailResponse)
|
||||
@router.get("/{dashboard_id:int}", response_model=DashboardDetailResponse)
|
||||
async def get_dashboard_detail(
|
||||
dashboard_id: int,
|
||||
env_id: str,
|
||||
@@ -337,60 +393,4 @@ async def backup_dashboards(
|
||||
raise HTTPException(status_code=503, detail=f"Failed to create backup task: {str(e)}")
|
||||
# [/DEF:backup_dashboards:Function]
|
||||
|
||||
# [DEF:DatabaseMapping:DataClass]
|
||||
class DatabaseMapping(BaseModel):
|
||||
source_db: str
|
||||
target_db: str
|
||||
source_db_uuid: Optional[str] = None
|
||||
target_db_uuid: Optional[str] = None
|
||||
confidence: float
|
||||
# [/DEF:DatabaseMapping:DataClass]
|
||||
|
||||
# [DEF:DatabaseMappingsResponse:DataClass]
|
||||
class DatabaseMappingsResponse(BaseModel):
|
||||
mappings: List[DatabaseMapping]
|
||||
# [/DEF:DatabaseMappingsResponse:DataClass]
|
||||
|
||||
# [DEF:get_database_mappings:Function]
|
||||
# @PURPOSE: Get database mapping suggestions between source and target environments
|
||||
# @PRE: User has permission plugin:migration:read
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @POST: Returns list of suggested database mappings with confidence scores
|
||||
# @PARAM: source_env_id (str) - Source environment ID
|
||||
# @PARAM: target_env_id (str) - Target environment ID
|
||||
# @RETURN: DatabaseMappingsResponse - List of suggested mappings
|
||||
# @RELATION: CALLS -> MappingService.get_suggestions
|
||||
@router.get("/db-mappings", response_model=DatabaseMappingsResponse)
|
||||
async def get_database_mappings(
|
||||
source_env_id: str,
|
||||
target_env_id: str,
|
||||
mapping_service=Depends(get_mapping_service),
|
||||
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||
):
|
||||
with belief_scope("get_database_mappings", f"source={source_env_id}, target={target_env_id}"):
|
||||
try:
|
||||
# Get mapping suggestions using MappingService
|
||||
suggestions = await mapping_service.get_suggestions(source_env_id, target_env_id)
|
||||
|
||||
# Format suggestions as DatabaseMapping objects
|
||||
mappings = [
|
||||
DatabaseMapping(
|
||||
source_db=s.get('source_db', ''),
|
||||
target_db=s.get('target_db', ''),
|
||||
source_db_uuid=s.get('source_db_uuid'),
|
||||
target_db_uuid=s.get('target_db_uuid'),
|
||||
confidence=s.get('confidence', 0.0)
|
||||
)
|
||||
for s in suggestions
|
||||
]
|
||||
|
||||
logger.info(f"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions")
|
||||
|
||||
return DatabaseMappingsResponse(mappings=mappings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}")
|
||||
raise HTTPException(status_code=503, detail=f"Failed to get database mappings: {str(e)}")
|
||||
# [/DEF:get_database_mappings:Function]
|
||||
|
||||
# [/DEF:backend.src.api.routes.dashboards:Module]
|
||||
|
||||
@@ -644,7 +644,12 @@
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{#each message.actions as action}
|
||||
<button
|
||||
class="rounded-md border border-slate-300 px-2.5 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
class="rounded-md border px-3 py-1.5 text-xs font-semibold transition
|
||||
{action.type === 'confirm'
|
||||
? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
: action.type === 'cancel'
|
||||
? 'border-rose-300 bg-rose-50 text-rose-700 hover:bg-rose-100'
|
||||
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
|
||||
on:click={() => handleAction(action, message)}
|
||||
>
|
||||
{action.label}
|
||||
|
||||
Reference in New Issue
Block a user