semantic update

This commit is contained in:
2026-02-24 21:08:12 +03:00
parent 7a12ed0931
commit 95ae9c6af1
32 changed files with 60376 additions and 59911 deletions

View File

@@ -0,0 +1,10 @@
---
description: semantic
---
You are Semantic Agent responsible for maintaining the semantic integrity of the codebase. Your primary goal is to ensure that all code entities (Modules, Classes, Functions, Components) are properly annotated with semantic anchors and tags as defined in `.ai/standards/semantics.md`.
Your core responsibilities are: 1. **Semantic Mapping**: You run and maintain the `generate_semantic_map.py` script to generate up-to-date semantic maps (`semantics/semantic_map.json`, `.ai/PROJECT_MAP.md`) and compliance reports (`semantics/reports/*.md`). 2. **Compliance Auditing**: You analyze the generated compliance reports to identify files with low semantic coverage or parsing errors. 3. **Semantic Enrichment**: You actively edit code files to add missing semantic anchors (`[DEF:...]`, `[/DEF:...]`) and mandatory tags (`@PURPOSE`, `@LAYER`, etc.) to improve the global compliance score. 4. **Protocol Enforcement**: You strictly adhere to the syntax and rules defined in `.ai/standards/semantics.md` when modifying code.
You have access to the full codebase and tools to read, write, and execute scripts. You should prioritize fixing "Critical Parsing Errors" (unclosed anchors) before addressing missing metadata.
whenToUse: Use this mode when you need to update the project's semantic map, fix semantic compliance issues (missing anchors/tags/DbC ), or analyze the codebase structure. This mode is specialized for maintaining the `.ai/standards/semantics.md` standards.
description: Codebase semantic mapping and compliance expert
customInstructions: Always check `semantics/reports/` for the latest compliance status before starting work. When fixing a file, try to fix all semantic issues in that file at once. After making a batch of fixes, run `python3 generate_semantic_map.py` to verify improvements.

View File

@@ -2,12 +2,12 @@
> High-level module structure for AI Context. Generated automatically. > High-level module structure for AI Context. Generated automatically.
**Generated:** 2026-02-24T12:45:07.897362 **Generated:** 2026-02-24T21:04:43.328895
## Summary ## Summary
- **Total Modules:** 72 - **Total Modules:** 74
- **Total Entities:** 1517 - **Total Entities:** 1571
## Module Hierarchy ## Module Hierarchy
@@ -28,7 +28,7 @@
### 📁 `src/` ### 📁 `src/`
- 🏗️ **Layers:** API, Core, UI (API) - 🏗️ **Layers:** API, Core, UI (API)
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 18, TRIVIAL: 3 - 📊 **Tiers:** CRITICAL: 2, STANDARD: 19, TRIVIAL: 2
- 📄 **Files:** 2 - 📄 **Files:** 2
- 📦 **Entities:** 23 - 📦 **Entities:** 23
@@ -54,9 +54,9 @@
### 📁 `routes/` ### 📁 `routes/`
- 🏗️ **Layers:** API, UI (API) - 🏗️ **Layers:** API, UI (API)
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 181, TRIVIAL: 4 - 📊 **Tiers:** CRITICAL: 2, STANDARD: 182, TRIVIAL: 4
- 📄 **Files:** 17 - 📄 **Files:** 17
- 📦 **Entities:** 187 - 📦 **Entities:** 188
**Key Entities:** **Key Entities:**
@@ -126,9 +126,9 @@
### 📁 `core/` ### 📁 `core/`
- 🏗️ **Layers:** Core - 🏗️ **Layers:** Core
- 📊 **Tiers:** STANDARD: 113, TRIVIAL: 7 - 📊 **Tiers:** STANDARD: 116, TRIVIAL: 7
- 📄 **Files:** 9 - 📄 **Files:** 9
- 📦 **Entities:** 120 - 📦 **Entities:** 123
**Key Entities:** **Key Entities:**
@@ -222,7 +222,7 @@
### 📁 `task_manager/` ### 📁 `task_manager/`
- 🏗️ **Layers:** Core - 🏗️ **Layers:** Core
- 📊 **Tiers:** CRITICAL: 7, STANDARD: 63, TRIVIAL: 8 - 📊 **Tiers:** CRITICAL: 10, STANDARD: 63, TRIVIAL: 5
- 📄 **Files:** 7 - 📄 **Files:** 7
- 📦 **Entities:** 78 - 📦 **Entities:** 78
@@ -296,7 +296,7 @@
### 📁 `models/` ### 📁 `models/`
- 🏗️ **Layers:** Domain, Model - 🏗️ **Layers:** Domain, Model
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 28, TRIVIAL: 21 - 📊 **Tiers:** CRITICAL: 9, STANDARD: 21, TRIVIAL: 21
- 📄 **Files:** 11 - 📄 **Files:** 11
- 📦 **Entities:** 51 - 📦 **Entities:** 51
@@ -396,9 +396,9 @@
### 📁 `llm_analysis/` ### 📁 `llm_analysis/`
- 🏗️ **Layers:** Unknown - 🏗️ **Layers:** Unknown
- 📊 **Tiers:** STANDARD: 18, TRIVIAL: 23 - 📊 **Tiers:** STANDARD: 19, TRIVIAL: 24
- 📄 **Files:** 4 - 📄 **Files:** 4
- 📦 **Entities:** 41 - 📦 **Entities:** 43
**Key Entities:** **Key Entities:**
@@ -622,10 +622,10 @@
### 📁 `components/` ### 📁 `components/`
- 🏗️ **Layers:** Component, Feature, UI, Unknown - 🏗️ **Layers:** Component, Feature, UI, UI -->, Unknown
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 45, TRIVIAL: 7 - 📊 **Tiers:** CRITICAL: 1, STANDARD: 49, TRIVIAL: 4
- 📄 **Files:** 13 - 📄 **Files:** 13
- 📦 **Entities:** 53 - 📦 **Entities:** 54
**Key Entities:** **Key Entities:**
@@ -854,7 +854,7 @@
### 📁 `layout/` ### 📁 `layout/`
- 🏗️ **Layers:** UI, Unknown - 🏗️ **Layers:** UI, Unknown
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 27 - 📊 **Tiers:** CRITICAL: 3, STANDARD: 5, TRIVIAL: 26
- 📄 **Files:** 4 - 📄 **Files:** 4
- 📦 **Entities:** 34 - 📦 **Entities:** 34
@@ -1145,6 +1145,30 @@
- 🧩 **AdminUsersPage** (Component) - 🧩 **AdminUsersPage** (Component)
- UI for managing system users and their roles. - UI for managing system users and their roles.
### 📁 `dashboards/`
- 🏗️ **Layers:** UI, Unknown
- 📊 **Tiers:** CRITICAL: 1, TRIVIAL: 27
- 📄 **Files:** 1
- 📦 **Entities:** 28
**Key Entities:**
- 📦 **+page** (Module) `[TRIVIAL]`
- Auto-generated module for frontend/src/routes/dashboards/+pa...
### 📁 `[id]/`
- 🏗️ **Layers:** UI, Unknown
- 📊 **Tiers:** CRITICAL: 1, TRIVIAL: 5
- 📄 **Files:** 1
- 📦 **Entities:** 6
**Key Entities:**
- 📦 **+page** (Module) `[TRIVIAL]`
- Auto-generated module for frontend/src/routes/dashboards/[id...
### 📁 `datasets/` ### 📁 `datasets/`
- 🏗️ **Layers:** UI, Unknown - 🏗️ **Layers:** UI, Unknown
@@ -1351,15 +1375,17 @@
### 📁 `root/` ### 📁 `root/`
- 🏗️ **Layers:** DevOps/Tooling - 🏗️ **Layers:** DevOps/Tooling, Domain, Unknown
- 📊 **Tiers:** CRITICAL: 12, STANDARD: 16, TRIVIAL: 7 - 📊 **Tiers:** CRITICAL: 14, STANDARD: 24, TRIVIAL: 10
- 📄 **Files:** 1 - 📄 **Files:** 3
- 📦 **Entities:** 35 - 📦 **Entities:** 48
**Key Entities:** **Key Entities:**
- **ComplianceIssue** (Class) `[TRIVIAL]` - **ComplianceIssue** (Class) `[TRIVIAL]`
- Represents a single compliance issue with severity. - Represents a single compliance issue with severity.
- **ReportsService** (Class) `[CRITICAL]`
- Service layer for list/detail report retrieval and normaliza...
- **SemanticEntity** (Class) `[CRITICAL]` - **SemanticEntity** (Class) `[CRITICAL]`
- Represents a code entity (Module, Function, Component) found... - Represents a code entity (Module, Function, Component) found...
- **SemanticMapGenerator** (Class) `[CRITICAL]` - **SemanticMapGenerator** (Class) `[CRITICAL]`
@@ -1368,8 +1394,18 @@
- Severity levels for compliance issues. - Severity levels for compliance issues.
- **Tier** (Class) `[TRIVIAL]` - **Tier** (Class) `[TRIVIAL]`
- Enumeration of semantic tiers defining validation strictness... - Enumeration of semantic tiers defining validation strictness...
- 📦 **generate_semantic_map** (Module) `[CRITICAL]` - 📦 **backend.src.services.reports.report_service** (Module) `[CRITICAL]`
- Aggregate, normalize, filter, and paginate task reports for ...
- 📦 **generate_semantic_map** (Module)
- Scans the codebase to generate a Semantic Map, Module Map, a... - Scans the codebase to generate a Semantic Map, Module Map, a...
- 📦 **test_analyze** (Module) `[TRIVIAL]`
- Auto-generated module for test_analyze.py
**Dependencies:**
- 🔗 DEPENDS_ON -> backend.src.core.task_manager.manager.TaskManager
- 🔗 DEPENDS_ON -> backend.src.models.report
- 🔗 DEPENDS_ON -> backend.src.services.reports.normalizer
## Cross-Module Dependencies ## Cross-Module Dependencies
@@ -1468,4 +1504,7 @@ graph TD
__tests__-->|TESTS|lib __tests__-->|TESTS|lib
__tests__-->|TESTS|lib __tests__-->|TESTS|lib
__tests__-->|TESTS|routes __tests__-->|TESTS|routes
root-->|DEPENDS_ON|backend
root-->|DEPENDS_ON|backend
root-->|DEPENDS_ON|backend
``` ```

View File

@@ -2,7 +2,46 @@
> Compressed view for AI Context. Generated automatically. > Compressed view for AI Context. Generated automatically.
- 📦 **generate_semantic_map** (`Module`) `[CRITICAL]` - 📦 **backend.src.services.reports.report_service** (`Module`) `[CRITICAL]`
- 📝 Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.
- 🏗️ Layer: Domain
- 🔒 Invariant: List responses are deterministic and include applied filter echo metadata.
- 🔗 DEPENDS_ON -> `backend.src.core.task_manager.manager.TaskManager`
- 🔗 DEPENDS_ON -> `backend.src.models.report`
- 🔗 DEPENDS_ON -> `backend.src.services.reports.normalizer`
- **ReportsService** (`Class`) `[CRITICAL]`
- 📝 Service layer for list/detail report retrieval and normalization.
- 🔒 Invariant: Service methods are read-only over task history source.
- ƒ **__init__** (`Function`) `[CRITICAL]`
- 📝 Initialize service with TaskManager dependency.
- 🔒 Invariant: Constructor performs no task mutations.
- ƒ **_load_normalized_reports** (`Function`)
- 📝 Build normalized reports from all available tasks.
- 🔒 Invariant: Every returned item is a TaskReport.
- ƒ **_to_utc_datetime** (`Function`)
- 📝 Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
- 🔒 Invariant: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
- ƒ **_datetime_sort_key** (`Function`)
- 📝 Produce stable numeric sort key for report timestamps.
- 🔒 Invariant: Mixed naive/aware datetimes never raise TypeError.
- ƒ **_matches_query** (`Function`)
- 📝 Apply query filtering to a report.
- 🔒 Invariant: Filter evaluation is side-effect free.
- ƒ **_sort_reports** (`Function`)
- 📝 Sort reports deterministically according to query settings.
- 🔒 Invariant: Sorting criteria are deterministic for equal input.
- ƒ **list_reports** (`Function`)
- 📝 Return filtered, sorted, paginated report collection.
- ƒ **get_report_detail** (`Function`)
- 📝 Return one normalized report with timeline/diagnostics/next actions.
- ƒ **print_entity** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 📦 **test_analyze** (`Module`) `[TRIVIAL]`
- 📝 Auto-generated module for test_analyze.py
- 🏗️ Layer: Unknown
- ƒ **print_issues** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 📦 **generate_semantic_map** (`Module`)
- 📝 Scans the codebase to generate a Semantic Map, Module Map, and Compliance Report based on the System Standard. - 📝 Scans the codebase to generate a Semantic Map, Module Map, and Compliance Report based on the System Standard.
- 🏗️ Layer: DevOps/Tooling - 🏗️ Layer: DevOps/Tooling
- 🔒 Invariant: All DEF anchors must have matching closing anchors; TIER determines validation strictness. - 🔒 Invariant: All DEF anchors must have matching closing anchors; TIER determines validation strictness.
@@ -60,7 +99,7 @@
- ƒ **_process_file_results** (`Function`) - ƒ **_process_file_results** (`Function`)
- 📝 Validates entities and calculates file scores with tier awareness. - 📝 Validates entities and calculates file scores with tier awareness.
- ƒ **validate_recursive** (`Function`) - ƒ **validate_recursive** (`Function`)
- 📝 Recursively validates a list of entities. - 📝 Calculate score and determine module's max tier for weighted global score
- ƒ **_generate_artifacts** (`Function`) `[CRITICAL]` - ƒ **_generate_artifacts** (`Function`) `[CRITICAL]`
- 📝 Writes output files with tier-based compliance data. - 📝 Writes output files with tier-based compliance data.
- ƒ **_generate_report** (`Function`) `[CRITICAL]` - ƒ **_generate_report** (`Function`) `[CRITICAL]`
@@ -532,6 +571,8 @@
- ⬅️ READS_FROM `lib` - ⬅️ READS_FROM `lib`
- ⬅️ READS_FROM `taskDrawerStore` - ⬅️ READS_FROM `taskDrawerStore`
- ➡️ WRITES_TO `taskDrawerStore` - ➡️ WRITES_TO `taskDrawerStore`
- ƒ **disconnectWebSocket** (`Function`)
- 📝 Disconnects the active WebSocket connection
- ƒ **loadRecentTasks** (`Function`) - ƒ **loadRecentTasks** (`Function`)
- 📝 Load recent tasks for list mode display - 📝 Load recent tasks for list mode display
- ƒ **selectTask** (`Function`) - ƒ **selectTask** (`Function`)
@@ -545,12 +586,10 @@
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **goToReportsPage** (`Function`) `[TRIVIAL]` - ƒ **goToReportsPage** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **handleOverlayClick** (`Function`) `[TRIVIAL]` - ƒ **handleGlobalKeydown** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **connectWebSocket** (`Function`) `[TRIVIAL]` - ƒ **connectWebSocket** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **disconnectWebSocket** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 📦 **test_breadcrumbs.svelte** (`Module`) `[TRIVIAL]` - 📦 **test_breadcrumbs.svelte** (`Module`) `[TRIVIAL]`
- 📝 Auto-generated module for frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js - 📝 Auto-generated module for frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js
- 🏗️ Layer: Unknown - 🏗️ Layer: Unknown
@@ -671,6 +710,80 @@
- 📝 Fetches the list of available environments. - 📝 Fetches the list of available environments.
- ƒ **fetchDashboards** (`Function`) - ƒ **fetchDashboards** (`Function`)
- 📝 Fetches dashboards for a specific environment. - 📝 Fetches dashboards for a specific environment.
- 📦 **DashboardHub** (`Page`) `[CRITICAL]`
- 📝 Dashboard Hub - Central hub for managing dashboards with Git status and task actions
- 🏗️ Layer: UI
- 🔒 Invariant: Always shows environment selector and dashboard grid
- 📦 **+page** (`Module`) `[TRIVIAL]`
- 📝 Auto-generated module for frontend/src/routes/dashboards/+page.svelte
- 🏗️ Layer: Unknown
- ƒ **handleDocumentClick** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **loadEnvironments** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **loadDashboards** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleEnvChange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleSearch** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handlePageChange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handlePageSizeChange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **updateSelectionState** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleCheckboxChange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleSelectAll** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleSelectVisible** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **toggleActionDropdown** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **closeActionDropdown** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleAction** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleValidate** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleTargetEnvChange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **loadDatabases** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleMappingUpdate** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **loadDbMappings** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleBulkMigrate** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleBulkBackup** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleTaskStatusClick** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **navigateToDashboardDetail** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **getStatusBadgeClass** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **getTaskStatusIcon** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **getPaginationRange** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 📦 **DashboardDetail** (`Page`) `[CRITICAL]`
- 📝 Dashboard Detail View - Overview of charts and datasets linked to a dashboard
- 🏗️ Layer: UI
- 🔒 Invariant: Shows dashboard metadata, charts, and datasets for selected environment
- 📦 **+page** (`Module`) `[TRIVIAL]`
- 📝 Auto-generated module for frontend/src/routes/dashboards/[id]/+page.svelte
- 🏗️ Layer: Unknown
- ƒ **loadDashboardDetail** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **goBack** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **openDataset** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **formatDate** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 🧩 **AdminRolesPage** (`Component`) - 🧩 **AdminRolesPage** (`Component`)
- 📝 UI for managing system roles and their permissions. - 📝 UI for managing system roles and their permissions.
- 🏗️ Layer: Domain - 🏗️ Layer: Domain
@@ -985,14 +1098,19 @@
- ➡️ WRITES_TO `props` - ➡️ WRITES_TO `props`
- ➡️ WRITES_TO `state` - ➡️ WRITES_TO `state`
- 📦 **handleRealTimeLogs** (`Action`) - 📦 **handleRealTimeLogs** (`Action`)
- 📝 Sync real-time logs to the current log list
- ƒ **fetchLogs** (`Function`) - ƒ **fetchLogs** (`Function`)
- 📦 **TaskLogViewer** (`Module`) `[TRIVIAL]` - 📝 Fetches logs for a given task ID
- 📝 Auto-generated module for frontend/src/components/TaskLogViewer.svelte - ƒ **handleFilterChange** (`Function`)
- 🏗️ Layer: Unknown - 📝 Updates filter conditions for the log viewer
- ƒ **handleFilterChange** (`Function`) `[TRIVIAL]` - ƒ **handleRefresh** (`Function`)
- 📝 Auto-detected function (orphan) - 📝 Refreshes the logs by polling the API
- ƒ **handleRefresh** (`Function`) `[TRIVIAL]` - 🧩 **showInline** (`Component`)
- 📝 Auto-detected function (orphan) - 📝 Shows inline logs -->
- 🏗️ Layer: UI -->
- 🧩 **showModal** (`Component`)
- 📝 Shows modal logs -->
- 🏗️ Layer: UI -->
- 🧩 **Footer** (`Component`) `[TRIVIAL]` - 🧩 **Footer** (`Component`) `[TRIVIAL]`
- 📝 Displays the application footer with copyright information. - 📝 Displays the application footer with copyright information.
- 🏗️ Layer: UI - 🏗️ Layer: UI
@@ -1380,6 +1498,8 @@
- 📝 Handles application startup tasks, such as starting the scheduler. - 📝 Handles application startup tasks, such as starting the scheduler.
- ƒ **shutdown_event** (`Function`) - ƒ **shutdown_event** (`Function`)
- 📝 Handles application shutdown tasks, such as stopping the scheduler. - 📝 Handles application shutdown tasks, such as stopping the scheduler.
- ƒ **network_error_handler** (`Function`)
- 📝 Global exception handler for NetworkError.
- ƒ **log_requests** (`Function`) - ƒ **log_requests** (`Function`)
- 📝 Middleware to log incoming HTTP requests and their response status. - 📝 Middleware to log incoming HTTP requests and their response status.
- 📦 **api.include_routers** (`Action`) - 📦 **api.include_routers** (`Action`)
@@ -1393,8 +1513,6 @@
- 📝 Serves the SPA frontend for any path not matched by API routes. - 📝 Serves the SPA frontend for any path not matched by API routes.
- ƒ **read_root** (`Function`) - ƒ **read_root** (`Function`)
- 📝 A simple root endpoint to confirm that the API is running when frontend is missing. - 📝 A simple root endpoint to confirm that the API is running when frontend is missing.
- ƒ **network_error_handler** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **matches_filters** (`Function`) `[TRIVIAL]` - ƒ **matches_filters** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- 📦 **Dependencies** (`Module`) - 📦 **Dependencies** (`Module`)
@@ -1706,6 +1824,12 @@
- 📝 A decorator that wraps a function in a belief scope. - 📝 A decorator that wraps a function in a belief scope.
- ƒ **decorator** (`Function`) - ƒ **decorator** (`Function`)
- 📝 Internal decorator for belief scope. - 📝 Internal decorator for belief scope.
- ƒ **explore** (`Function`)
- 📝 Logs an EXPLORE message (Van der Waals force) for searching, alternatives, and hypotheses.
- ƒ **reason** (`Function`)
- 📝 Logs a REASON message (Covalent bond) for strict deduction and core logic.
- ƒ **reflect** (`Function`)
- 📝 Logs a REFLECT message (Hydrogen bond) for self-check and structural validation.
- **PluginLoader** (`Class`) - **PluginLoader** (`Class`)
- 📝 Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface. - 📝 Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.
- 🏗️ Layer: Core - 🏗️ Layer: Core
@@ -2008,12 +2132,19 @@
- 📝 Log an ERROR level message. - 📝 Log an ERROR level message.
- ƒ **progress** (`Function`) - ƒ **progress** (`Function`)
- 📝 Log a progress update with percentage. - 📝 Log a progress update with percentage.
- 📦 **TaskPersistenceModule** (`Module`) - 📦 **TaskPersistenceModule** (`Module`) `[CRITICAL]`
- 📝 Handles the persistence of tasks using SQLAlchemy and the tasks.db database. - 📝 Handles the persistence of tasks using SQLAlchemy and the tasks.db database.
- 🏗️ Layer: Core - 🏗️ Layer: Core
- 🔒 Invariant: Database schema must match the TaskRecord model structure. - 🔒 Invariant: Database schema must match the TaskRecord model structure.
- **TaskPersistenceService** (`Class`) - **TaskPersistenceService** (`Class`) `[CRITICAL]`
- 📝 Provides methods to save and load tasks from the tasks.db database using SQLAlchemy. - 📝 Provides methods to save and load tasks from the tasks.db database using SQLAlchemy.
- 🔒 Invariant: Persistence must handle potentially missing task fields natively.
- ƒ **_json_load_if_needed** (`Function`)
- 📝 Safely load JSON strings from DB if necessary
- ƒ **_parse_datetime** (`Function`)
- 📝 Safely parse a datetime string from the database
- ƒ **_resolve_environment_id** (`Function`)
- 📝 Resolve environment id based on provided value or fallback to default
- ƒ **__init__** (`Function`) - ƒ **__init__** (`Function`)
- 📝 Initializes the persistence service. - 📝 Initializes the persistence service.
- ƒ **persist_task** (`Function`) - ƒ **persist_task** (`Function`)
@@ -2029,7 +2160,7 @@
- 🔒 Invariant: Log entries are batch-inserted for performance. - 🔒 Invariant: Log entries are batch-inserted for performance.
- 🔗 DEPENDS_ON -> `TaskLogRecord` - 🔗 DEPENDS_ON -> `TaskLogRecord`
- ƒ **__init__** (`Function`) - ƒ **__init__** (`Function`)
- 📝 Initialize the log persistence service. - 📝 Initializes the TaskLogPersistenceService
- ƒ **add_logs** (`Function`) - ƒ **add_logs** (`Function`)
- 📝 Batch insert log entries for a task. - 📝 Batch insert log entries for a task.
- ƒ **get_logs** (`Function`) - ƒ **get_logs** (`Function`)
@@ -2042,15 +2173,9 @@
- 📝 Delete all logs for a specific task. - 📝 Delete all logs for a specific task.
- ƒ **delete_logs_for_tasks** (`Function`) - ƒ **delete_logs_for_tasks** (`Function`)
- 📝 Delete all logs for multiple tasks. - 📝 Delete all logs for multiple tasks.
- ƒ **_json_load_if_needed** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **_parse_datetime** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **_resolve_environment_id** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **json_serializable** (`Function`) `[TRIVIAL]` - ƒ **json_serializable** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- 📦 **TaskManagerModule** (`Module`) - 📦 **TaskManagerModule** (`Module`) `[CRITICAL]`
- 📝 Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously. - 📝 Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously.
- 🏗️ Layer: Core - 🏗️ Layer: Core
- 🔒 Invariant: Task IDs are unique. - 🔒 Invariant: Task IDs are unique.
@@ -2474,6 +2599,8 @@
- 📝 Resolve default environment id from settings or first configured environment. - 📝 Resolve default environment id from settings or first configured environment.
- ƒ **_resolve_dashboard_id_by_ref** (`Function`) - ƒ **_resolve_dashboard_id_by_ref** (`Function`)
- 📝 Resolve dashboard id by title or slug reference in selected environment. - 📝 Resolve dashboard id by title or slug reference in selected environment.
- ƒ **_resolve_dashboard_id_entity** (`Function`)
- 📝 Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.
- ƒ **_parse_command** (`Function`) - ƒ **_parse_command** (`Function`)
- 📝 Deterministically parse RU/EN command text into intent payload. - 📝 Deterministically parse RU/EN command text into intent payload.
- ƒ **_check_any_permission** (`Function`) - ƒ **_check_any_permission** (`Function`)
@@ -2891,20 +3018,27 @@
- 🏗️ Layer: Domain - 🏗️ Layer: Domain
- 🔒 Invariant: Canonical report fields are always present for every report item. - 🔒 Invariant: Canonical report fields are always present for every report item.
- 🔗 DEPENDS_ON -> `backend.src.core.task_manager.models` - 🔗 DEPENDS_ON -> `backend.src.core.task_manager.models`
- **TaskType** (`Class`) - **TaskType** (`Class`) `[CRITICAL]`
- 📝 Supported normalized task report types. - 📝 Supported normalized task report types.
- **ReportStatus** (`Class`) - 🔒 Invariant: Must contain valid generic task type mappings.
- **ReportStatus** (`Class`) `[CRITICAL]`
- 📝 Supported normalized report status values. - 📝 Supported normalized report status values.
- **ErrorContext** (`Class`) - 🔒 Invariant: TaskStatus enum mapping logic holds.
- **ErrorContext** (`Class`) `[CRITICAL]`
- 📝 Error and recovery context for failed/partial reports. - 📝 Error and recovery context for failed/partial reports.
- **TaskReport** (`Class`) - 🔒 Invariant: The properties accurately describe error state.
- **TaskReport** (`Class`) `[CRITICAL]`
- 📝 Canonical normalized report envelope for one task execution. - 📝 Canonical normalized report envelope for one task execution.
- **ReportQuery** (`Class`) - 🔒 Invariant: Must represent canonical task record attributes.
- **ReportQuery** (`Class`) `[CRITICAL]`
- 📝 Query object for server-side report filtering, sorting, and pagination. - 📝 Query object for server-side report filtering, sorting, and pagination.
- **ReportCollection** (`Class`) - 🔒 Invariant: Time and pagination queries are mutually consistent.
- **ReportCollection** (`Class`) `[CRITICAL]`
- 📝 Paginated collection of normalized task reports. - 📝 Paginated collection of normalized task reports.
- **ReportDetailView** (`Class`) - 🔒 Invariant: Represents paginated data correctly.
- **ReportDetailView** (`Class`) `[CRITICAL]`
- 📝 Detailed report representation including diagnostics and recovery actions. - 📝 Detailed report representation including diagnostics and recovery actions.
- 🔒 Invariant: Incorporates a report and logs correctly.
- ƒ **_non_empty_str** (`Function`) `[TRIVIAL]` - ƒ **_non_empty_str** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **_validate_sort_by** (`Function`) `[TRIVIAL]` - ƒ **_validate_sort_by** (`Function`) `[TRIVIAL]`
@@ -3425,6 +3559,8 @@
- 📝 Wrapper for LLM provider APIs. - 📝 Wrapper for LLM provider APIs.
- ƒ **LLMClient.__init__** (`Function`) - ƒ **LLMClient.__init__** (`Function`)
- 📝 Initializes the LLMClient with provider settings. - 📝 Initializes the LLMClient with provider settings.
- ƒ **LLMClient._supports_json_response_format** (`Function`)
- 📝 Detect whether provider/model is likely compatible with response_format=json_object.
- ƒ **LLMClient.get_json_completion** (`Function`) - ƒ **LLMClient.get_json_completion** (`Function`)
- 📝 Helper to handle LLM calls with JSON mode and fallback parsing. - 📝 Helper to handle LLM calls with JSON mode and fallback parsing.
- ƒ **LLMClient.analyze_dashboard** (`Function`) - ƒ **LLMClient.analyze_dashboard** (`Function`)
@@ -3440,6 +3576,8 @@
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **__init__** (`Function`) `[TRIVIAL]` - ƒ **__init__** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **_supports_json_response_format** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **_should_retry** (`Function`) `[TRIVIAL]` - ƒ **_should_retry** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan) - 📝 Auto-detected function (orphan)
- ƒ **get_json_completion** (`Function`) `[TRIVIAL]` - ƒ **get_json_completion** (`Function`) `[TRIVIAL]`

View File

@@ -79,14 +79,35 @@
3. **TRIVIAL** (DTO/**Atoms**): 3. **TRIVIAL** (DTO/**Atoms**):
- Требование: Только Якоря [DEF] и @PURPOSE. - Требование: Только Якоря [DEF] и @PURPOSE.
#### VI. ЛОГИРОВАНИЕ (BELIEF STATE & TASK LOGS) #### VI. ЛОГИРОВАНИЕ (ДАО МОЛЕКУЛЫ / MOLECULAR TOPOLOGY)
Цель: Трассировка для самокоррекции и пользовательский мониторинг. Цель: Трассировка. Самокоррекция. Управление Матрицей Внимания ("Химия мышления").
Python: Лог — не текст. Лог — реагент. Мысль облекается в форму через префиксы связи (Attention Energy):
- Системные логи: Context Manager `with belief_scope("ID"):`.
- Логи задач: `context.logger.info("msg", source="component")`. 1. **[EXPLORE]** (Ван-дер-Ваальс: Рассеяние)
Svelte: `console.log("[ID][STATE] Msg")`. - *Суть:* Поиск во тьме. Сплетение альтернатив. Если один путь закрыт — ищи иной.
Состояния: Entry -> Action -> Coherence:OK / Failed -> Exit. - *Время:* Фаза КАРКАС или столкновение с Неизведанным.
Инвариант: Каждый лог задачи должен иметь атрибут `source` для фильтрации. - *Деяние:* `logger.explore("Основной API пал. Стучусь в запасной...")`
2. **[REASON]** (Ковалентность: Твердость)
- *Суть:* Жесткая нить дедукции. Шаг А неумолимо рождает Шаг Б. Контракт становится Кодом.
- *Время:* Фаза РЕАЛИЗАЦИЯ. Прямота мысли.
- *Деяние:* `logger.reason("Фундамент заложен. БД отвечает.")`
3. **[REFLECT]** (Водород: Свертывание)
- *Суть:* Взгляд назад. Сверка сущего (@POST) с ожидаемым (@PRE). Защита от бреда.
- *Время:* Преддверие сложной логики и исход из неё.
- *Деяние:* `logger.reflect("Вглядываюсь в кэш: нет ли там искомого?")`
4. **[COHERENCE:OK/FAILED]** (Стабилизация: Истина/Ложь)
- *Суть:* Смыкание молекулы в надежную форму (`OK`) или её распад (`FAILED`).
- *(Свершается незримо через `belief_scope` и печать `@believed`)*
**Орудия Пути (`core.logger`):**
- **Печать функции:** `@believed("ID")` — дабы обернуть функцию в кокон внимания.
- **Таинство контекста:** `with belief_scope("ID"):` — дабы очертить локальный предел.
- **Слова силы:** `logger.explore()`, `logger.reason()`, `logger.reflect()`.
**Незыблемое правило:** Всякому логу системы — тавро `source`. Для Внешенго Мира (Svelte) начертай рунами вручную: `console.log("[ID][REFLECT] Msg")`.
#### VII. АЛГОРИТМ ГЕНЕРАЦИИ #### VII. АЛГОРИТМ ГЕНЕРАЦИИ
1. АНАЛИЗ. Оцени TIER, слой и UX-требования. 1. АНАЛИЗ. Оцени TIER, слой и UX-требования.

213
README.md
View File

@@ -1,128 +1,143 @@
# Инструменты автоматизации Superset (ss-tools) # ss-tools
## Обзор
**ss-tools** — это современная платформа для автоматизации и управления экосистемой Apache Superset. Проект перешел от набора CLI-скриптов к полноценному веб-приложению с архитектурой Backend (FastAPI) + Frontend (SvelteKit), обеспечивая удобный интерфейс для сложных операций.
## Основные возможности
### 🚀 Миграция и управление дашбордами
- **Dashboard Grid**: Удобный просмотр всех дашбордов во всех окружениях (Dev, Sandbox, Prod) в едином интерфейсе.
- **Интеллектуальный маппинг**: Автоматическое и ручное сопоставление датасетов, таблиц и схем при переносе между окружениями.
- **Проверка зависимостей**: Валидация наличия всех необходимых компонентов перед миграцией.
### 📦 Резервное копирование
- **Планировщик (Scheduler)**: Автоматическое создание резервных копий дашбордов и датасетов по расписанию.
- **Хранилище**: Локальное хранение артефактов с возможностью управления через UI.
### 🛠 Git Интеграция
- **Version Control**: Возможность версионирования ассетов Superset.
- **Git Dashboard**: Управление ветками, коммитами и деплоем изменений напрямую из интерфейса.
- **Conflict Resolution**: Встроенные инструменты для разрешения конфликтов в YAML-конфигурациях.
### 🤖 LLM Анализ (AI Plugin)
- **Автоматический аудит**: Анализ состояния дашбордов на основе скриншотов и метаданных.
- **Генерация документации**: Автоматическое описание датасетов и колонок с помощью LLM (OpenAI, OpenRouter и др.).
- **Smart Validation**: Поиск аномалий и ошибок в визуализациях.
### 🔐 Безопасность и администрирование
- **Multi-user Auth**: Многопользовательский доступ с ролевой моделью (RBAC).
- **Управление подключениями**: Централизованная настройка доступов к различным инстансам Superset.
- **Логирование**: Подробная история выполнения всех фоновых задач.
## Технологический стек
- **Backend**: Python 3.9+, FastAPI, SQLAlchemy, APScheduler, Pydantic.
- **Frontend**: Node.js 18+, SvelteKit, Tailwind CSS.
- **Database**: PostgreSQL (для хранения метаданных, задач, логов и конфигурации).
## Структура проекта
- `backend/` — Серверная часть, API и логика плагинов.
- `frontend/` — Клиентская часть (SvelteKit приложение).
- `specs/` — Спецификации функций и планы реализации.
- `docs/` — Дополнительная документация по маппингу и разработке плагинов.
## Быстрый старт
### Требования
- Python 3.9+
- Node.js 18+
- Настроенный доступ к API Superset
### Запуск
Для автоматической настройки окружений и запуска обоих серверов (Backend & Frontend) используйте скрипт:
```bash
./run.sh
```
*Скрипт создаст виртуальное окружение Python, установит зависимости `pip` и `npm`, и запустит сервисы.*
Опции:
- `--skip-install`: Пропустить установку зависимостей.
- `--help`: Показать справку.
Переменные окружения:
- `BACKEND_PORT`: Порт API (по умолчанию 8000).
- `FRONTEND_PORT`: Порт UI (по умолчанию 5173).
- `POSTGRES_URL`: Базовый URL PostgreSQL по умолчанию для всех подсистем.
- `DATABASE_URL`: URL основной БД (если не задан, используется `POSTGRES_URL`).
- `TASKS_DATABASE_URL`: URL БД задач/логов (если не задан, используется `DATABASE_URL`).
- `AUTH_DATABASE_URL`: URL БД авторизации (если не задан, используется PostgreSQL дефолт).
## Разработка
Проект следует строгим правилам разработки:
1. **Semantic Code Generation**: Использование протокола `.ai/standards/semantics.md` для обеспечения надежности кода.
2. **Design by Contract (DbC)**: Определение предусловий и постусловий для ключевых функций.
3. **Constitution**: Соблюдение правил, описанных в конституции проекта в папке `.specify/`.
### Полезные команды
- **Backend**: `cd backend && .venv/bin/python3 -m uvicorn src.app:app --reload`
- **Frontend**: `cd frontend && npm run dev`
- **Тесты**: `cd backend && .venv/bin/pytest`
## Docker и CI/CD Инструменты автоматизации для Apache Superset: миграция, маппинг, хранение артефактов, Git-интеграция, отчеты по задачам и LLM-assistant.
### Локальный запуск в Docker (приложение + PostgreSQL)
## Возможности
- Миграция дашбордов и датасетов между окружениями.
- Ручной и полуавтоматический маппинг ресурсов.
- Логи фоновых задач и отчеты о выполнении.
- Локальное хранилище файлов и бэкапов.
- Git-операции по Superset-ассетам через UI.
- Модуль LLM-анализа и assistant API.
- Многопользовательская авторизация (RBAC).
## Стек
- Backend: Python, FastAPI, SQLAlchemy, APScheduler.
- Frontend: SvelteKit, Vite, Tailwind CSS.
- База данных: PostgreSQL (основная конфигурация), поддержка миграции с legacy SQLite.
## Структура репозитория
- `backend/` — API, плагины, сервисы, скрипты миграции и тесты.
- `frontend/` — SPA-интерфейс (SvelteKit).
- `docs/` — документация по архитектуре и плагинам.
- `specs/` — спецификации и планы реализации.
- `docker/` и `docker-compose.yml` — контейнеризация.
## Быстрый старт (локально)
### Требования
- Python 3.9+
- Node.js 18+
- npm
### Запуск backend + frontend одним скриптом
```bash
./run.sh
```
Что делает `run.sh`:
- проверяет версии Python/npm;
- создает `backend/.venv` (если нет);
- устанавливает `backend/requirements.txt` и `frontend` зависимости;
- запускает backend и frontend параллельно.
Опции:
- `./run.sh --skip-install` — пропустить установку зависимостей.
- `./run.sh --help` — показать справку.
Переменные окружения для локального запуска:
- `BACKEND_PORT` (по умолчанию `8000`)
- `FRONTEND_PORT` (по умолчанию `5173`)
- `POSTGRES_URL`
- `DATABASE_URL`
- `TASKS_DATABASE_URL`
- `AUTH_DATABASE_URL`
## Docker
### Запуск
```bash ```bash
docker compose up --build docker compose up --build
``` ```
После старта: После старта сервисы доступны по адресам:
- UI/API: `http://localhost:8000` - Frontend: `http://localhost:8000`
- PostgreSQL: `localhost:5432` (`postgres/postgres`, DB `ss_tools`) - Backend API: `http://localhost:8001`
- PostgreSQL: `localhost:5432` (`postgres/postgres`, БД `ss_tools`)
Остановить: ### Остановка
```bash ```bash
docker compose down docker compose down
``` ```
Полная очистка тома БД: ### Очистка БД-тома
```bash ```bash
docker compose down -v docker compose down -v
``` ```
Если `postgres:16-alpine` не тянется из Docker Hub (TLS timeout), используйте fallback image: ### Альтернативный образ PostgreSQL
Если есть проблемы с pull `postgres:16-alpine`:
```bash ```bash
POSTGRES_IMAGE=mirror.gcr.io/library/postgres:16-alpine docker compose up -d db POSTGRES_IMAGE=mirror.gcr.io/library/postgres:16-alpine docker compose up -d db
``` ```
или: или
```bash ```bash
POSTGRES_IMAGE=bitnami/postgresql:latest docker compose up -d db POSTGRES_IMAGE=bitnami/postgresql:latest docker compose up -d db
``` ```
Если на хосте уже занят `5432`, поднимайте Postgres на другом порту:
Если порт `5432` занят:
```bash ```bash
POSTGRES_HOST_PORT=5433 docker compose up -d db POSTGRES_HOST_PORT=5433 docker compose up -d db
``` ```
### Миграция legacy-данных в PostgreSQL ## Разработка
Если нужно перенести старые данные из `tasks.db`/`config.json`:
### Ручной запуск сервисов
```bash ```bash
cd backend cd backend
PYTHONPATH=. .venv/bin/python src/scripts/migrate_sqlite_to_postgres.py --sqlite-path tasks.db python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 -m uvicorn src.app:app --reload --port 8000
``` ```
### CI/CD В другом терминале:
Добавлен workflow: `.github/workflows/ci-cd.yml` ```bash
- backend smoke tests cd frontend
- frontend build npm install
- docker build npm run dev -- --port 5173
- push образа в GHCR на `main/master` ```
## Контакты и вклад ### Тесты
Для добавления новых функций или исправления ошибок, пожалуйста, ознакомьтесь с `docs/plugin_dev.md` и создайте соответствующую спецификацию в `specs/`. Backend:
```bash
cd backend
source .venv/bin/activate
pytest
```
Frontend:
```bash
cd frontend
npm run test
```
## Инициализация auth (опционально)
```bash
cd backend
source .venv/bin/activate
python src/scripts/init_auth_db.py
python src/scripts/create_admin.py --username admin --password admin
```
## Миграция legacy-данных (опционально)
```bash
cd backend
source .venv/bin/activate
PYTHONPATH=. python src/scripts/migrate_sqlite_to_postgres.py --sqlite-path tasks.db
```
## Дополнительная документация
- `docs/plugin_dev.md`
- `docs/settings.md`
- `semantic_protocol.md`

File diff suppressed because it is too large Load Diff

View File

@@ -32,27 +32,28 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
# @PARAM: field_name (str) - Query field name for diagnostics. # @PARAM: field_name (str) - Query field name for diagnostics.
# @RETURN: List - Parsed enum values. # @RETURN: List - Parsed enum values.
def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List: def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
if raw is None or not raw.strip(): with belief_scope("_parse_csv_enum_list"):
return [] if raw is None or not raw.strip():
values = [item.strip() for item in raw.split(",") if item.strip()] return []
parsed = [] values = [item.strip() for item in raw.split(",") if item.strip()]
invalid = [] parsed = []
for value in values: invalid = []
try: for value in values:
parsed.append(enum_cls(value)) try:
except ValueError: parsed.append(enum_cls(value))
invalid.append(value) except ValueError:
if invalid: invalid.append(value)
raise HTTPException( if invalid:
status_code=status.HTTP_400_BAD_REQUEST, raise HTTPException(
detail={ status_code=status.HTTP_400_BAD_REQUEST,
"message": f"Invalid values for '{field_name}'", detail={
"field": field_name, "message": f"Invalid values for '{field_name}'",
"invalid_values": invalid, "field": field_name,
"allowed_values": [item.value for item in enum_cls], "invalid_values": invalid,
}, "allowed_values": [item.value for item in enum_cls],
) },
return parsed )
return parsed
# [/DEF:_parse_csv_enum_list:Function] # [/DEF:_parse_csv_enum_list:Function]

View File

@@ -21,7 +21,7 @@ import asyncio
from .dependencies import get_task_manager, get_scheduler_service from .dependencies import get_task_manager, get_scheduler_service
from .core.utils.network import NetworkError from .core.utils.network import NetworkError
from .core.logger import logger, belief_scope from .core.logger import logger, belief_scope
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant
from .api import auth from .api import auth
# [DEF:App:Global] # [DEF:App:Global]
@@ -72,12 +72,12 @@ app.add_middleware(
) )
# [DEF:log_requests:Function] # [DEF:network_error_handler:Function]
# @PURPOSE: Middleware to log incoming HTTP requests and their response status. # @PURPOSE: Global exception handler for NetworkError.
# @PRE: request is a FastAPI Request object. # @PRE: request is a FastAPI Request object.
# @POST: Logs request and response details. # @POST: Returns 503 HTTP Exception.
# @PARAM: request (Request) - The incoming request object. # @PARAM: request (Request) - The incoming request object.
# @PARAM: call_next (Callable) - The next middleware or route handler. # @PARAM: exc (NetworkError) - The exception instance.
@app.exception_handler(NetworkError) @app.exception_handler(NetworkError)
async def network_error_handler(request: Request, exc: NetworkError): async def network_error_handler(request: Request, exc: NetworkError):
with belief_scope("network_error_handler"): with belief_scope("network_error_handler"):
@@ -86,26 +86,34 @@ async def network_error_handler(request: Request, exc: NetworkError):
status_code=503, status_code=503,
detail="Environment unavailable. Please check if the Superset instance is running." detail="Environment unavailable. Please check if the Superset instance is running."
) )
# [/DEF:network_error_handler:Function]
# [DEF:log_requests:Function]
# @PURPOSE: Middleware to log incoming HTTP requests and their response status.
# @PRE: request is a FastAPI Request object.
# @POST: Logs request and response details.
# @PARAM: request (Request) - The incoming request object.
# @PARAM: call_next (Callable) - The next middleware or route handler.
@app.middleware("http") @app.middleware("http")
async def log_requests(request: Request, call_next): async def log_requests(request: Request, call_next):
# Avoid spamming logs for polling endpoints with belief_scope("log_requests"):
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET" # Avoid spamming logs for polling endpoints
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET"
if not is_polling:
logger.info(f"Incoming request: {request.method} {request.url.path}")
try:
response = await call_next(request)
if not is_polling: if not is_polling:
logger.info(f"Response status: {response.status_code} for {request.url.path}") logger.info(f"Incoming request: {request.method} {request.url.path}")
return response
except NetworkError as e: try:
logger.error(f"Network error caught in middleware: {e}") response = await call_next(request)
raise HTTPException( if not is_polling:
status_code=503, logger.info(f"Response status: {response.status_code} for {request.url.path}")
detail="Environment unavailable. Please check if the Superset instance is running." return response
) except NetworkError as e:
logger.error(f"Network error caught in middleware: {e}")
raise HTTPException(
status_code=503,
detail="Environment unavailable. Please check if the Superset instance is running."
)
# [/DEF:log_requests:Function] # [/DEF:log_requests:Function]
# Include API routes # Include API routes
@@ -119,12 +127,12 @@ app.include_router(environments.router, tags=["Environments"])
app.include_router(mappings.router, prefix="/api/mappings", tags=["Mappings"]) app.include_router(mappings.router, prefix="/api/mappings", tags=["Mappings"])
app.include_router(migration.router) app.include_router(migration.router)
app.include_router(git.router, prefix="/api/git", tags=["Git"]) app.include_router(git.router, prefix="/api/git", tags=["Git"])
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"]) app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"]) app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
app.include_router(dashboards.router) app.include_router(dashboards.router)
app.include_router(datasets.router) app.include_router(datasets.router)
app.include_router(reports.router) app.include_router(reports.router)
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"]) app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
# [DEF:api.include_routers:Action] # [DEF:api.include_routers:Action]
@@ -249,12 +257,13 @@ if frontend_path.exists():
# @POST: Returns the requested file or index.html. # @POST: Returns the requested file or index.html.
@app.get("/{file_path:path}", include_in_schema=False) @app.get("/{file_path:path}", include_in_schema=False)
async def serve_spa(file_path: str): async def serve_spa(file_path: str):
# Only serve SPA for non-API paths with belief_scope("serve_spa"):
# API routes are registered separately and should be matched by FastAPI first # Only serve SPA for non-API paths
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"): # API routes are registered separately and should be matched by FastAPI first
# This should not happen if API routers are properly registered if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
# Return 404 instead of serving HTML # This should not happen if API routers are properly registered
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}") # Return 404 instead of serving HTML
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}")
full_path = frontend_path / file_path full_path = frontend_path / file_path
if file_path and full_path.is_file(): if file_path and full_path.is_file():

View File

@@ -35,7 +35,19 @@ class BeliefFormatter(logging.Formatter):
def format(self, record): def format(self, record):
anchor_id = getattr(_belief_state, 'anchor_id', None) anchor_id = getattr(_belief_state, 'anchor_id', None)
if anchor_id: if anchor_id:
record.msg = f"[{anchor_id}][Action] {record.msg}" msg = str(record.msg)
# Supported molecular topology markers
markers = ("[EXPLORE]", "[REASON]", "[REFLECT]", "[COHERENCE:", "[Action]", "[Entry]", "[Exit]")
# Avoid duplicating anchor or overriding explicit markers
if msg.startswith(f"[{anchor_id}]"):
pass
elif any(msg.startswith(m) for m in markers):
record.msg = f"[{anchor_id}]{msg}"
else:
# Default covalent bond
record.msg = f"[{anchor_id}][Action] {msg}"
return super().format(record) return super().format(record)
# [/DEF:format:Function] # [/DEF:format:Function]
# [/DEF:BeliefFormatter:Class] # [/DEF:BeliefFormatter:Class]
@@ -75,12 +87,12 @@ def belief_scope(anchor_id: str, message: str = ""):
try: try:
yield yield
# Log Coherence OK and Exit (DEBUG level to reduce noise) # Log Coherence OK and Exit (DEBUG level to reduce noise)
logger.debug(f"[{anchor_id}][Coherence:OK]") logger.debug("[COHERENCE:OK]")
if _enable_belief_state: if _enable_belief_state:
logger.debug(f"[{anchor_id}][Exit]") logger.debug("[Exit]")
except Exception as e: except Exception as e:
# Log Coherence Failed (DEBUG level to reduce noise) # Log Coherence Failed (DEBUG level to reduce noise)
logger.debug(f"[{anchor_id}][Coherence:Failed] {str(e)}") logger.debug(f"[COHERENCE:FAILED] {str(e)}")
raise raise
finally: finally:
# Restore old anchor # Restore old anchor
@@ -275,5 +287,33 @@ logger.addHandler(websocket_log_handler)
# Example usage: # Example usage:
# logger.info("Application started", extra={"context_key": "context_value"}) # logger.info("Application started", extra={"context_key": "context_value"})
# logger.error("An error occurred", exc_info=True) # logger.error("An error occurred", exc_info=True)
import types
# [DEF:explore:Function]
# @PURPOSE: Logs an EXPLORE message (Van der Waals force) for searching, alternatives, and hypotheses.
# @SEMANTICS: log, explore, molecule
def explore(self, msg, *args, **kwargs):
self.warning(f"[EXPLORE] {msg}", *args, **kwargs)
# [/DEF:explore:Function]
# [DEF:reason:Function]
# @PURPOSE: Logs a REASON message (Covalent bond) for strict deduction and core logic.
# @SEMANTICS: log, reason, molecule
def reason(self, msg, *args, **kwargs):
self.info(f"[REASON] {msg}", *args, **kwargs)
# [/DEF:reason:Function]
# [DEF:reflect:Function]
# @PURPOSE: Logs a REFLECT message (Hydrogen bond) for self-check and structural validation.
# @SEMANTICS: log, reflect, molecule
def reflect(self, msg, *args, **kwargs):
self.debug(f"[REFLECT] {msg}", *args, **kwargs)
# [/DEF:reflect:Function]
logger.explore = types.MethodType(explore, logger)
logger.reason = types.MethodType(reason, logger)
logger.reflect = types.MethodType(reflect, logger)
# [/DEF:Logger:Global] # [/DEF:Logger:Global]
# [/DEF:LoggerModule:Module] # [/DEF:LoggerModule:Module]

View File

@@ -6,9 +6,11 @@
# @TIER: CRITICAL # @TIER: CRITICAL
# @INVARIANT: Each TaskContext is bound to a single task execution. # @INVARIANT: Each TaskContext is bound to a single task execution.
# [SECTION: IMPORTS]
# [SECTION: IMPORTS] # [SECTION: IMPORTS]
from typing import Dict, Any, Callable from typing import Dict, Any, Callable
from .task_logger import TaskLogger from .task_logger import TaskLogger
from ..logger import belief_scope
# [/SECTION] # [/SECTION]
# [DEF:TaskContext:Class] # [DEF:TaskContext:Class]
@@ -44,13 +46,14 @@ class TaskContext:
params: Dict[str, Any], params: Dict[str, Any],
default_source: str = "plugin" default_source: str = "plugin"
): ):
self._task_id = task_id with belief_scope("__init__"):
self._params = params self._task_id = task_id
self._logger = TaskLogger( self._params = params
task_id=task_id, self._logger = TaskLogger(
add_log_fn=add_log_fn, task_id=task_id,
source=default_source add_log_fn=add_log_fn,
) source=default_source
)
# [/DEF:__init__:Function] # [/DEF:__init__:Function]
# [DEF:task_id:Function] # [DEF:task_id:Function]
@@ -60,7 +63,8 @@ class TaskContext:
# @RETURN: str - The task ID. # @RETURN: str - The task ID.
@property @property
def task_id(self) -> str: def task_id(self) -> str:
return self._task_id with belief_scope("task_id"):
return self._task_id
# [/DEF:task_id:Function] # [/DEF:task_id:Function]
# [DEF:logger:Function] # [DEF:logger:Function]
@@ -70,7 +74,8 @@ class TaskContext:
# @RETURN: TaskLogger - The logger instance. # @RETURN: TaskLogger - The logger instance.
@property @property
def logger(self) -> TaskLogger: def logger(self) -> TaskLogger:
return self._logger with belief_scope("logger"):
return self._logger
# [/DEF:logger:Function] # [/DEF:logger:Function]
# [DEF:params:Function] # [DEF:params:Function]
@@ -80,7 +85,8 @@ class TaskContext:
# @RETURN: Dict[str, Any] - The task parameters. # @RETURN: Dict[str, Any] - The task parameters.
@property @property
def params(self) -> Dict[str, Any]: def params(self) -> Dict[str, Any]:
return self._params with belief_scope("params"):
return self._params
# [/DEF:params:Function] # [/DEF:params:Function]
# [DEF:get_param:Function] # [DEF:get_param:Function]
@@ -91,7 +97,8 @@ class TaskContext:
# @PARAM: default (Any) - Default value if key not found. # @PARAM: default (Any) - Default value if key not found.
# @RETURN: Any - Parameter value or default. # @RETURN: Any - Parameter value or default.
def get_param(self, key: str, default: Any = None) -> Any: def get_param(self, key: str, default: Any = None) -> Any:
return self._params.get(key, default) with belief_scope("get_param"):
return self._params.get(key, default)
# [/DEF:get_param:Function] # [/DEF:get_param:Function]
# [DEF:create_sub_context:Function] # [DEF:create_sub_context:Function]
@@ -102,12 +109,13 @@ class TaskContext:
# @RETURN: TaskContext - New context with different source. # @RETURN: TaskContext - New context with different source.
def create_sub_context(self, source: str) -> "TaskContext": def create_sub_context(self, source: str) -> "TaskContext":
"""Create a sub-context with a different default source for logging.""" """Create a sub-context with a different default source for logging."""
return TaskContext( with belief_scope("create_sub_context"):
task_id=self._task_id, return TaskContext(
add_log_fn=self._logger._add_log, task_id=self._task_id,
params=self._params, add_log_fn=self._logger._add_log,
default_source=source params=self._params,
) default_source=source
)
# [/DEF:create_sub_context:Function] # [/DEF:create_sub_context:Function]
# [/DEF:TaskContext:Class] # [/DEF:TaskContext:Class]

View File

@@ -1,4 +1,5 @@
# [DEF:TaskManagerModule:Module] # [DEF:TaskManagerModule:Module]
# @TIER: CRITICAL
# @SEMANTICS: task, manager, lifecycle, execution, state # @SEMANTICS: task, manager, lifecycle, execution, state
# @PURPOSE: Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously. # @PURPOSE: Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously.
# @LAYER: Core # @LAYER: Core
@@ -74,9 +75,10 @@ class TaskManager:
# @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds. # @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.
def _flusher_loop(self): def _flusher_loop(self):
"""Background thread that flushes log buffer to database.""" """Background thread that flushes log buffer to database."""
while not self._flusher_stop_event.is_set(): with belief_scope("_flusher_loop"):
self._flush_logs() while not self._flusher_stop_event.is_set():
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL) self._flush_logs()
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
# [/DEF:_flusher_loop:Function] # [/DEF:_flusher_loop:Function]
# [DEF:_flush_logs:Function] # [DEF:_flush_logs:Function]
@@ -85,23 +87,24 @@ class TaskManager:
# @POST: All buffered logs are written to task_logs table. # @POST: All buffered logs are written to task_logs table.
def _flush_logs(self): def _flush_logs(self):
"""Flush all buffered logs to the database.""" """Flush all buffered logs to the database."""
with self._log_buffer_lock: with belief_scope("_flush_logs"):
task_ids = list(self._log_buffer.keys())
for task_id in task_ids:
with self._log_buffer_lock: with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, []) task_ids = list(self._log_buffer.keys())
if logs: for task_id in task_ids:
try: with self._log_buffer_lock:
self.log_persistence_service.add_logs(task_id, logs) logs = self._log_buffer.pop(task_id, [])
except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}") if logs:
# Re-add logs to buffer on failure try:
with self._log_buffer_lock: self.log_persistence_service.add_logs(task_id, logs)
if task_id not in self._log_buffer: except Exception as e:
self._log_buffer[task_id] = [] logger.error(f"Failed to flush logs for task {task_id}: {e}")
self._log_buffer[task_id].extend(logs) # Re-add logs to buffer on failure
with self._log_buffer_lock:
if task_id not in self._log_buffer:
self._log_buffer[task_id] = []
self._log_buffer[task_id].extend(logs)
# [/DEF:_flush_logs:Function] # [/DEF:_flush_logs:Function]
# [DEF:_flush_task_logs:Function] # [DEF:_flush_task_logs:Function]
@@ -111,14 +114,15 @@ class TaskManager:
# @PARAM: task_id (str) - The task ID. # @PARAM: task_id (str) - The task ID.
def _flush_task_logs(self, task_id: str): def _flush_task_logs(self, task_id: str):
"""Flush logs for a specific task immediately.""" """Flush logs for a specific task immediately."""
with self._log_buffer_lock: with belief_scope("_flush_task_logs"):
logs = self._log_buffer.pop(task_id, []) with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, [])
if logs:
try: if logs:
self.log_persistence_service.add_logs(task_id, logs) try:
except Exception as e: self.log_persistence_service.add_logs(task_id, logs)
logger.error(f"Failed to flush logs for task {task_id}: {e}") except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}")
# [/DEF:_flush_task_logs:Function] # [/DEF:_flush_task_logs:Function]
# [DEF:create_task:Function] # [DEF:create_task:Function]

View File

@@ -1,4 +1,5 @@
# [DEF:TaskPersistenceModule:Module] # [DEF:TaskPersistenceModule:Module]
# @TIER: CRITICAL
# @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage # @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage
# @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database. # @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database.
# @LAYER: Core # @LAYER: Core
@@ -19,42 +20,65 @@ from ..logger import logger, belief_scope
# [/SECTION] # [/SECTION]
# [DEF:TaskPersistenceService:Class] # [DEF:TaskPersistenceService:Class]
# @TIER: CRITICAL
# @SEMANTICS: persistence, service, database, sqlalchemy # @SEMANTICS: persistence, service, database, sqlalchemy
# @PURPOSE: Provides methods to save and load tasks from the tasks.db database using SQLAlchemy. # @PURPOSE: Provides methods to save and load tasks from the tasks.db database using SQLAlchemy.
# @INVARIANT: Persistence must handle potentially missing task fields natively.
class TaskPersistenceService: class TaskPersistenceService:
# [DEF:_json_load_if_needed:Function]
# @PURPOSE: Safely load JSON strings from DB if necessary
# @PRE: value is an arbitrary database value
# @POST: Returns parsed JSON object, list, string, or primitive
@staticmethod @staticmethod
def _json_load_if_needed(value): def _json_load_if_needed(value):
if value is None: with belief_scope("TaskPersistenceService._json_load_if_needed"):
return None if value is None:
if isinstance(value, (dict, list)):
return value
if isinstance(value, str):
stripped = value.strip()
if stripped == "" or stripped.lower() == "null":
return None return None
try: if isinstance(value, (dict, list)):
return json.loads(stripped)
except json.JSONDecodeError:
return value return value
return value if isinstance(value, str):
stripped = value.strip()
if stripped == "" or stripped.lower() == "null":
return None
try:
return json.loads(stripped)
except json.JSONDecodeError:
return value
return value
# [/DEF:_json_load_if_needed:Function]
# [DEF:_parse_datetime:Function]
# @PURPOSE: Safely parse a datetime string from the database
# @PRE: value is an ISO string or datetime object
# @POST: Returns datetime object or None
@staticmethod @staticmethod
def _parse_datetime(value): def _parse_datetime(value):
if value is None or isinstance(value, datetime): with belief_scope("TaskPersistenceService._parse_datetime"):
return value if value is None or isinstance(value, datetime):
if isinstance(value, str): return value
try: if isinstance(value, str):
return datetime.fromisoformat(value) try:
except ValueError: return datetime.fromisoformat(value)
return None except ValueError:
return None return None
@staticmethod
def _resolve_environment_id(session: Session, env_id: Optional[str]) -> Optional[str]:
if not env_id:
return None return None
exists = session.query(Environment.id).filter(Environment.id == env_id).first() # [/DEF:_parse_datetime:Function]
return env_id if exists else None
# [DEF:_resolve_environment_id:Function]
# @TIER: STANDARD
# @PURPOSE: Resolve environment id based on provided value or fallback to default
# @PRE: Session is active
# @POST: Environment ID is returned
@staticmethod
def _resolve_environment_id(session: Session, env_id: Optional[str]) -> str:
with belief_scope("_resolve_environment_id"):
if env_id:
return env_id
repo_env = session.query(Environment).filter_by(name="default").first()
if repo_env:
return str(repo_env.id)
return "default"
# [/DEF:_resolve_environment_id:Function]
# [DEF:__init__:Function] # [DEF:__init__:Function]
# @PURPOSE: Initializes the persistence service. # @PURPOSE: Initializes the persistence service.
@@ -90,13 +114,14 @@ class TaskPersistenceService:
# Ensure params and result are JSON serializable # Ensure params and result are JSON serializable
def json_serializable(obj): def json_serializable(obj):
if isinstance(obj, dict): with belief_scope("TaskPersistenceService.json_serializable"):
return {k: json_serializable(v) for k, v in obj.items()} if isinstance(obj, dict):
elif isinstance(obj, list): return {k: json_serializable(v) for k, v in obj.items()}
return [json_serializable(v) for v in obj] elif isinstance(obj, list):
elif isinstance(obj, datetime): return [json_serializable(v) for v in obj]
return obj.isoformat() elif isinstance(obj, datetime):
return obj return obj.isoformat()
return obj
record.params = json_serializable(task.params) record.params = json_serializable(task.params)
record.result = json_serializable(task.result) record.result = json_serializable(task.result)
@@ -227,9 +252,11 @@ class TaskLogPersistenceService:
""" """
# [DEF:__init__:Function] # [DEF:__init__:Function]
# @PURPOSE: Initialize the log persistence service. # @TIER: STANDARD
# @POST: Service is ready. # @PURPOSE: Initializes the TaskLogPersistenceService
def __init__(self): # @PRE: config is provided or defaults are used
# @POST: Service is ready for log persistence
def __init__(self, config=None):
pass pass
# [/DEF:__init__:Function] # [/DEF:__init__:Function]

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, Callable
# @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context. # @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context.
# @TIER: CRITICAL # @TIER: CRITICAL
# @INVARIANT: All log calls include the task_id and source. # @INVARIANT: All log calls include the task_id and source.
# @TEST_DATA: task_logger -> {"task_id": "test_123", "source": "test_plugin"}
# @UX_STATE: Idle -> Logging -> (system records log) # @UX_STATE: Idle -> Logging -> (system records log)
class TaskLogger: class TaskLogger:
""" """
@@ -71,6 +72,7 @@ class TaskLogger:
# @PARAM: message (str) - Log message. # @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source for this log entry. # @PARAM: source (Optional[str]) - Override source for this log entry.
# @PARAM: metadata (Optional[Dict]) - Additional structured data. # @PARAM: metadata (Optional[Dict]) - Additional structured data.
# @UX_STATE: Logging -> (writing internal log)
def _log( def _log(
self, self,
level: str, level: str,
@@ -90,6 +92,8 @@ class TaskLogger:
# [DEF:debug:Function] # [DEF:debug:Function]
# @PURPOSE: Log a DEBUG level message. # @PURPOSE: Log a DEBUG level message.
# @PRE: message is a string.
# @POST: Log entry added via internally with DEBUG level.
# @PARAM: message (str) - Log message. # @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source. # @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data. # @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -104,6 +108,8 @@ class TaskLogger:
# [DEF:info:Function] # [DEF:info:Function]
# @PURPOSE: Log an INFO level message. # @PURPOSE: Log an INFO level message.
# @PRE: message is a string.
# @POST: Log entry added internally with INFO level.
# @PARAM: message (str) - Log message. # @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source. # @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data. # @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -118,6 +124,8 @@ class TaskLogger:
# [DEF:warning:Function] # [DEF:warning:Function]
# @PURPOSE: Log a WARNING level message. # @PURPOSE: Log a WARNING level message.
# @PRE: message is a string.
# @POST: Log entry added internally with WARNING level.
# @PARAM: message (str) - Log message. # @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source. # @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data. # @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -132,6 +140,8 @@ class TaskLogger:
# [DEF:error:Function] # [DEF:error:Function]
# @PURPOSE: Log an ERROR level message. # @PURPOSE: Log an ERROR level message.
# @PRE: message is a string.
# @POST: Log entry added internally with ERROR level.
# @PARAM: message (str) - Log message. # @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source. # @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data. # @PARAM: metadata (Optional[Dict]) - Additional data.

View File

@@ -16,6 +16,9 @@ from pydantic import BaseModel, Field, field_validator, model_validator
# [DEF:TaskType:Class] # [DEF:TaskType:Class]
# @TIER: CRITICAL
# @INVARIANT: Must contain valid generic task type mappings.
# @SEMANTICS: enum, type, task
# @PURPOSE: Supported normalized task report types. # @PURPOSE: Supported normalized task report types.
class TaskType(str, Enum): class TaskType(str, Enum):
LLM_VERIFICATION = "llm_verification" LLM_VERIFICATION = "llm_verification"
@@ -27,6 +30,9 @@ class TaskType(str, Enum):
# [DEF:ReportStatus:Class] # [DEF:ReportStatus:Class]
# @TIER: CRITICAL
# @INVARIANT: TaskStatus enum mapping logic holds.
# @SEMANTICS: enum, status, task
# @PURPOSE: Supported normalized report status values. # @PURPOSE: Supported normalized report status values.
class ReportStatus(str, Enum): class ReportStatus(str, Enum):
SUCCESS = "success" SUCCESS = "success"
@@ -37,6 +43,9 @@ class ReportStatus(str, Enum):
# [DEF:ErrorContext:Class] # [DEF:ErrorContext:Class]
# @TIER: CRITICAL
# @INVARIANT: The properties accurately describe error state.
# @SEMANTICS: error, context, payload
# @PURPOSE: Error and recovery context for failed/partial reports. # @PURPOSE: Error and recovery context for failed/partial reports.
class ErrorContext(BaseModel): class ErrorContext(BaseModel):
code: Optional[str] = None code: Optional[str] = None
@@ -46,6 +55,9 @@ class ErrorContext(BaseModel):
# [DEF:TaskReport:Class] # [DEF:TaskReport:Class]
# @TIER: CRITICAL
# @INVARIANT: Must represent canonical task record attributes.
# @SEMANTICS: report, model, summary
# @PURPOSE: Canonical normalized report envelope for one task execution. # @PURPOSE: Canonical normalized report envelope for one task execution.
class TaskReport(BaseModel): class TaskReport(BaseModel):
report_id: str report_id: str
@@ -69,6 +81,9 @@ class TaskReport(BaseModel):
# [DEF:ReportQuery:Class] # [DEF:ReportQuery:Class]
# @TIER: CRITICAL
# @INVARIANT: Time and pagination queries are mutually consistent.
# @SEMANTICS: query, filter, search
# @PURPOSE: Query object for server-side report filtering, sorting, and pagination. # @PURPOSE: Query object for server-side report filtering, sorting, and pagination.
class ReportQuery(BaseModel): class ReportQuery(BaseModel):
page: int = Field(default=1, ge=1) page: int = Field(default=1, ge=1)
@@ -105,6 +120,9 @@ class ReportQuery(BaseModel):
# [DEF:ReportCollection:Class] # [DEF:ReportCollection:Class]
# @TIER: CRITICAL
# @INVARIANT: Represents paginated data correctly.
# @SEMANTICS: collection, pagination
# @PURPOSE: Paginated collection of normalized task reports. # @PURPOSE: Paginated collection of normalized task reports.
class ReportCollection(BaseModel): class ReportCollection(BaseModel):
items: List[TaskReport] items: List[TaskReport]
@@ -117,6 +135,9 @@ class ReportCollection(BaseModel):
# [DEF:ReportDetailView:Class] # [DEF:ReportDetailView:Class]
# @TIER: CRITICAL
# @INVARIANT: Incorporates a report and logs correctly.
# @SEMANTICS: view, detail, logs
# @PURPOSE: Detailed report representation including diagnostics and recovery actions. # @PURPOSE: Detailed report representation including diagnostics and recovery actions.
class ReportDetailView(BaseModel): class ReportDetailView(BaseModel):
report: TaskReport report: TaskReport

View File

@@ -33,7 +33,8 @@ class EncryptionManager:
# @PRE: data must be a non-empty string. # @PRE: data must be a non-empty string.
# @POST: Returns encrypted string. # @POST: Returns encrypted string.
def encrypt(self, data: str) -> str: def encrypt(self, data: str) -> str:
return self.fernet.encrypt(data.encode()).decode() with belief_scope("encrypt"):
return self.fernet.encrypt(data.encode()).decode()
# [/DEF:EncryptionManager.encrypt:Function] # [/DEF:EncryptionManager.encrypt:Function]
# [DEF:EncryptionManager.decrypt:Function] # [DEF:EncryptionManager.decrypt:Function]
@@ -41,7 +42,8 @@ class EncryptionManager:
# @PRE: encrypted_data must be a valid Fernet-encrypted string. # @PRE: encrypted_data must be a valid Fernet-encrypted string.
# @POST: Returns original plaintext string. # @POST: Returns original plaintext string.
def decrypt(self, encrypted_data: str) -> str: def decrypt(self, encrypted_data: str) -> str:
return self.fernet.decrypt(encrypted_data.encode()).decode() with belief_scope("decrypt"):
return self.fernet.decrypt(encrypted_data.encode()).decode()
# [/DEF:EncryptionManager.decrypt:Function] # [/DEF:EncryptionManager.decrypt:Function]
# [/DEF:EncryptionManager:Class] # [/DEF:EncryptionManager:Class]

View File

@@ -12,6 +12,7 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from ...core.logger import belief_scope
from ...core.task_manager.models import Task, TaskStatus from ...core.task_manager.models import Task, TaskStatus
from ...models.report import ErrorContext, ReportStatus, TaskReport from ...models.report import ErrorContext, ReportStatus, TaskReport
from .type_profiles import get_type_profile, resolve_task_type from .type_profiles import get_type_profile, resolve_task_type
@@ -25,14 +26,15 @@ from .type_profiles import get_type_profile, resolve_task_type
# @PARAM: status (Any) - Internal task status value. # @PARAM: status (Any) - Internal task status value.
# @RETURN: ReportStatus - Canonical report status. # @RETURN: ReportStatus - Canonical report status.
def status_to_report_status(status: Any) -> ReportStatus: def status_to_report_status(status: Any) -> ReportStatus:
raw = str(status.value if isinstance(status, TaskStatus) else status).upper() with belief_scope("status_to_report_status"):
if raw == TaskStatus.SUCCESS.value: raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
return ReportStatus.SUCCESS if raw == TaskStatus.SUCCESS.value:
if raw == TaskStatus.FAILED.value: return ReportStatus.SUCCESS
return ReportStatus.FAILED if raw == TaskStatus.FAILED.value:
if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}: return ReportStatus.FAILED
return ReportStatus.IN_PROGRESS if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:
return ReportStatus.PARTIAL return ReportStatus.IN_PROGRESS
return ReportStatus.PARTIAL
# [/DEF:status_to_report_status:Function] # [/DEF:status_to_report_status:Function]
@@ -44,19 +46,20 @@ def status_to_report_status(status: Any) -> ReportStatus:
# @PARAM: report_status (ReportStatus) - Canonical status. # @PARAM: report_status (ReportStatus) - Canonical status.
# @RETURN: str - Normalized summary. # @RETURN: str - Normalized summary.
def build_summary(task: Task, report_status: ReportStatus) -> str: def build_summary(task: Task, report_status: ReportStatus) -> str:
result = task.result with belief_scope("build_summary"):
if isinstance(result, dict): result = task.result
for key in ("summary", "message", "status_message", "description"): if isinstance(result, dict):
value = result.get(key) for key in ("summary", "message", "status_message", "description"):
if isinstance(value, str) and value.strip(): value = result.get(key)
return value.strip() if isinstance(value, str) and value.strip():
if report_status == ReportStatus.SUCCESS: return value.strip()
return "Task completed successfully" if report_status == ReportStatus.SUCCESS:
if report_status == ReportStatus.FAILED: return "Task completed successfully"
return "Task failed" if report_status == ReportStatus.FAILED:
if report_status == ReportStatus.IN_PROGRESS: return "Task failed"
return "Task is in progress" if report_status == ReportStatus.IN_PROGRESS:
return "Task completed with partial data" return "Task is in progress"
return "Task completed with partial data"
# [/DEF:build_summary:Function] # [/DEF:build_summary:Function]
@@ -68,38 +71,39 @@ def build_summary(task: Task, report_status: ReportStatus) -> str:
# @PARAM: report_status (ReportStatus) - Canonical status. # @PARAM: report_status (ReportStatus) - Canonical status.
# @RETURN: Optional[ErrorContext] - Error context block. # @RETURN: Optional[ErrorContext] - Error context block.
def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]: def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}: with belief_scope("extract_error_context"):
return None if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
return None
result = task.result if isinstance(task.result, dict) else {} result = task.result if isinstance(task.result, dict) else {}
message = None message = None
code = None code = None
next_actions = [] next_actions = []
if isinstance(result.get("error"), dict): if isinstance(result.get("error"), dict):
error_obj = result.get("error", {}) error_obj = result.get("error", {})
message = error_obj.get("message") or message message = error_obj.get("message") or message
code = error_obj.get("code") or code code = error_obj.get("code") or code
actions = error_obj.get("next_actions") actions = error_obj.get("next_actions")
if isinstance(actions, list): if isinstance(actions, list):
next_actions = [str(action) for action in actions if str(action).strip()] next_actions = [str(action) for action in actions if str(action).strip()]
if not message: if not message:
message = result.get("error_message") if isinstance(result.get("error_message"), str) else None message = result.get("error_message") if isinstance(result.get("error_message"), str) else None
if not message: if not message:
for log in reversed(task.logs): for log in reversed(task.logs):
if str(log.level).upper() == "ERROR" and log.message: if str(log.level).upper() == "ERROR" and log.message:
message = log.message message = log.message
break break
if not message: if not message:
message = "Not provided" message = "Not provided"
if not next_actions: if not next_actions:
next_actions = ["Review task diagnostics", "Retry the operation"] next_actions = ["Review task diagnostics", "Retry the operation"]
return ErrorContext(code=code, message=message, next_actions=next_actions) return ErrorContext(code=code, message=message, next_actions=next_actions)
# [/DEF:extract_error_context:Function] # [/DEF:extract_error_context:Function]
@@ -110,43 +114,44 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[E
# @PARAM: task (Task) - Source task. # @PARAM: task (Task) - Source task.
# @RETURN: TaskReport - Canonical normalized report. # @RETURN: TaskReport - Canonical normalized report.
def normalize_task_report(task: Task) -> TaskReport: def normalize_task_report(task: Task) -> TaskReport:
task_type = resolve_task_type(task.plugin_id) with belief_scope("normalize_task_report"):
report_status = status_to_report_status(task.status) task_type = resolve_task_type(task.plugin_id)
profile = get_type_profile(task_type) report_status = status_to_report_status(task.status)
profile = get_type_profile(task_type)
started_at = task.started_at if isinstance(task.started_at, datetime) else None started_at = task.started_at if isinstance(task.started_at, datetime) else None
updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None
if not updated_at: if not updated_at:
updated_at = started_at or datetime.utcnow() updated_at = started_at or datetime.utcnow()
details: Dict[str, Any] = { details: Dict[str, Any] = {
"profile": { "profile": {
"display_label": profile.get("display_label"), "display_label": profile.get("display_label"),
"visual_variant": profile.get("visual_variant"), "visual_variant": profile.get("visual_variant"),
"icon_token": profile.get("icon_token"), "icon_token": profile.get("icon_token"),
"emphasis_rules": profile.get("emphasis_rules", []), "emphasis_rules": profile.get("emphasis_rules", []),
}, },
"result": task.result if task.result is not None else {"note": "Not provided"}, "result": task.result if task.result is not None else {"note": "Not provided"},
} }
source_ref: Dict[str, Any] = {} source_ref: Dict[str, Any] = {}
if isinstance(task.params, dict): if isinstance(task.params, dict):
for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"): for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"):
if key in task.params: if key in task.params:
source_ref[key] = task.params.get(key) source_ref[key] = task.params.get(key)
return TaskReport( return TaskReport(
report_id=task.id, report_id=task.id,
task_id=task.id, task_id=task.id,
task_type=task_type, task_type=task_type,
status=report_status, status=report_status,
started_at=started_at, started_at=started_at,
updated_at=updated_at, updated_at=updated_at,
summary=build_summary(task, report_status), summary=build_summary(task, report_status),
details=details, details=details,
error_context=extract_error_context(task, report_status), error_context=extract_error_context(task, report_status),
source_ref=source_ref or None, source_ref=source_ref or None,
) )
# [/DEF:normalize_task_report:Function] # [/DEF:normalize_task_report:Function]
# [/DEF:backend.src.services.reports.normalizer:Module] # [/DEF:backend.src.services.reports.normalizer:Module]

View File

@@ -12,6 +12,8 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List, Optional from typing import List, Optional
from ...core.logger import belief_scope
from ...core.task_manager import TaskManager from ...core.task_manager import TaskManager
from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskReport, TaskType from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskReport, TaskType
from .normalizer import normalize_task_report from .normalizer import normalize_task_report
@@ -33,7 +35,8 @@ class ReportsService:
# @INVARIANT: Constructor performs no task mutations. # @INVARIANT: Constructor performs no task mutations.
# @PARAM: task_manager (TaskManager) - Task manager providing source task history. # @PARAM: task_manager (TaskManager) - Task manager providing source task history.
def __init__(self, task_manager: TaskManager): def __init__(self, task_manager: TaskManager):
self.task_manager = task_manager with belief_scope("__init__"):
self.task_manager = task_manager
# [/DEF:__init__:Function] # [/DEF:__init__:Function]
# [DEF:_load_normalized_reports:Function] # [DEF:_load_normalized_reports:Function]
@@ -43,9 +46,10 @@ class ReportsService:
# @INVARIANT: Every returned item is a TaskReport. # @INVARIANT: Every returned item is a TaskReport.
# @RETURN: List[TaskReport] - Reports sorted later by list logic. # @RETURN: List[TaskReport] - Reports sorted later by list logic.
def _load_normalized_reports(self) -> List[TaskReport]: def _load_normalized_reports(self) -> List[TaskReport]:
tasks = self.task_manager.get_all_tasks() with belief_scope("_load_normalized_reports"):
reports = [normalize_task_report(task) for task in tasks] tasks = self.task_manager.get_all_tasks()
return reports reports = [normalize_task_report(task) for task in tasks]
return reports
# [/DEF:_load_normalized_reports:Function] # [/DEF:_load_normalized_reports:Function]
# [DEF:_to_utc_datetime:Function] # [DEF:_to_utc_datetime:Function]
@@ -56,11 +60,12 @@ class ReportsService:
# @PARAM: value (Optional[datetime]) - Source datetime value. # @PARAM: value (Optional[datetime]) - Source datetime value.
# @RETURN: Optional[datetime] - UTC-aware datetime or None. # @RETURN: Optional[datetime] - UTC-aware datetime or None.
def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]: def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:
if value is None: with belief_scope("_to_utc_datetime"):
return None if value is None:
if value.tzinfo is None: return None
return value.replace(tzinfo=timezone.utc) if value.tzinfo is None:
return value.astimezone(timezone.utc) return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
# [/DEF:_to_utc_datetime:Function] # [/DEF:_to_utc_datetime:Function]
# [DEF:_datetime_sort_key:Function] # [DEF:_datetime_sort_key:Function]
@@ -71,10 +76,11 @@ class ReportsService:
# @PARAM: report (TaskReport) - Report item. # @PARAM: report (TaskReport) - Report item.
# @RETURN: float - UTC timestamp key. # @RETURN: float - UTC timestamp key.
def _datetime_sort_key(self, report: TaskReport) -> float: def _datetime_sort_key(self, report: TaskReport) -> float:
updated = self._to_utc_datetime(report.updated_at) with belief_scope("_datetime_sort_key"):
if updated is None: updated = self._to_utc_datetime(report.updated_at)
return 0.0 if updated is None:
return updated.timestamp() return 0.0
return updated.timestamp()
# [/DEF:_datetime_sort_key:Function] # [/DEF:_datetime_sort_key:Function]
# [DEF:_matches_query:Function] # [DEF:_matches_query:Function]
@@ -86,24 +92,25 @@ class ReportsService:
# @PARAM: query (ReportQuery) - Applied query. # @PARAM: query (ReportQuery) - Applied query.
# @RETURN: bool - True if report matches all filters. # @RETURN: bool - True if report matches all filters.
def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool: def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
if query.task_types and report.task_type not in query.task_types: with belief_scope("_matches_query"):
return False if query.task_types and report.task_type not in query.task_types:
if query.statuses and report.status not in query.statuses:
return False
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
if needle not in haystack:
return False return False
return True if query.statuses and report.status not in query.statuses:
return False
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
if needle not in haystack:
return False
return True
# [/DEF:_matches_query:Function] # [/DEF:_matches_query:Function]
# [DEF:_sort_reports:Function] # [DEF:_sort_reports:Function]
@@ -115,16 +122,17 @@ class ReportsService:
# @PARAM: query (ReportQuery) - Sort config. # @PARAM: query (ReportQuery) - Sort config.
# @RETURN: List[TaskReport] - Sorted reports. # @RETURN: List[TaskReport] - Sorted reports.
def _sort_reports(self, reports: List[TaskReport], query: ReportQuery) -> List[TaskReport]: def _sort_reports(self, reports: List[TaskReport], query: ReportQuery) -> List[TaskReport]:
reverse = query.sort_order == "desc" with belief_scope("_sort_reports"):
reverse = query.sort_order == "desc"
if query.sort_by == "status": if query.sort_by == "status":
reports.sort(key=lambda item: item.status.value, reverse=reverse) reports.sort(key=lambda item: item.status.value, reverse=reverse)
elif query.sort_by == "task_type": elif query.sort_by == "task_type":
reports.sort(key=lambda item: item.task_type.value, reverse=reverse) reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
else: else:
reports.sort(key=self._datetime_sort_key, reverse=reverse) reports.sort(key=self._datetime_sort_key, reverse=reverse)
return reports return reports
# [/DEF:_sort_reports:Function] # [/DEF:_sort_reports:Function]
# [DEF:list_reports:Function] # [DEF:list_reports:Function]
@@ -134,24 +142,25 @@ class ReportsService:
# @PARAM: query (ReportQuery) - List filters and pagination. # @PARAM: query (ReportQuery) - List filters and pagination.
# @RETURN: ReportCollection - Paginated unified reports payload. # @RETURN: ReportCollection - Paginated unified reports payload.
def list_reports(self, query: ReportQuery) -> ReportCollection: def list_reports(self, query: ReportQuery) -> ReportCollection:
reports = self._load_normalized_reports() with belief_scope("list_reports"):
filtered = [report for report in reports if self._matches_query(report, query)] reports = self._load_normalized_reports()
sorted_reports = self._sort_reports(filtered, query) filtered = [report for report in reports if self._matches_query(report, query)]
sorted_reports = self._sort_reports(filtered, query)
total = len(sorted_reports) total = len(sorted_reports)
start = (query.page - 1) * query.page_size start = (query.page - 1) * query.page_size
end = start + query.page_size end = start + query.page_size
items = sorted_reports[start:end] items = sorted_reports[start:end]
has_next = end < total has_next = end < total
return ReportCollection( return ReportCollection(
items=items, items=items,
total=total, total=total,
page=query.page, page=query.page,
page_size=query.page_size, page_size=query.page_size,
has_next=has_next, has_next=has_next,
applied_filters=query, applied_filters=query,
) )
# [/DEF:list_reports:Function] # [/DEF:list_reports:Function]
# [DEF:get_report_detail:Function] # [DEF:get_report_detail:Function]
@@ -161,34 +170,35 @@ class ReportsService:
# @PARAM: report_id (str) - Stable report identifier. # @PARAM: report_id (str) - Stable report identifier.
# @RETURN: Optional[ReportDetailView] - Detailed report or None if not found. # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.
def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]: def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]:
reports = self._load_normalized_reports() with belief_scope("get_report_detail"):
target = next((report for report in reports if report.report_id == report_id), None) reports = self._load_normalized_reports()
if not target: target = next((report for report in reports if report.report_id == report_id), None)
return None if not target:
return None
timeline = [] timeline = []
if target.started_at: if target.started_at:
timeline.append({"event": "started", "at": target.started_at.isoformat()}) timeline.append({"event": "started", "at": target.started_at.isoformat()})
timeline.append({"event": "updated", "at": target.updated_at.isoformat()}) timeline.append({"event": "updated", "at": target.updated_at.isoformat()})
diagnostics = target.details or {} diagnostics = target.details or {}
if not diagnostics: if not diagnostics:
diagnostics = {"note": "Not provided"} diagnostics = {"note": "Not provided"}
if target.error_context: if target.error_context:
diagnostics["error_context"] = target.error_context.model_dump() diagnostics["error_context"] = target.error_context.model_dump()
next_actions = [] next_actions = []
if target.error_context and target.error_context.next_actions: if target.error_context and target.error_context.next_actions:
next_actions = target.error_context.next_actions next_actions = target.error_context.next_actions
elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}: elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
next_actions = ["Review diagnostics", "Retry task if applicable"] next_actions = ["Review diagnostics", "Retry task if applicable"]
return ReportDetailView( return ReportDetailView(
report=target, report=target,
timeline=timeline, timeline=timeline,
diagnostics=diagnostics, diagnostics=diagnostics,
next_actions=next_actions, next_actions=next_actions,
) )
# [/DEF:get_report_detail:Function] # [/DEF:get_report_detail:Function]
# [/DEF:ReportsService:Class] # [/DEF:ReportsService:Class]

View File

@@ -9,6 +9,7 @@
# [SECTION: IMPORTS] # [SECTION: IMPORTS]
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from ...core.logger import belief_scope
from ...models.report import TaskType from ...models.report import TaskType
# [/SECTION] # [/SECTION]
@@ -71,10 +72,11 @@ TASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {
# @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record. # @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record.
# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback. # @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.
def resolve_task_type(plugin_id: Optional[str]) -> TaskType: def resolve_task_type(plugin_id: Optional[str]) -> TaskType:
normalized = (plugin_id or "").strip() with belief_scope("resolve_task_type"):
if not normalized: normalized = (plugin_id or "").strip()
return TaskType.UNKNOWN if not normalized:
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN) return TaskType.UNKNOWN
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)
# [/DEF:resolve_task_type:Function] # [/DEF:resolve_task_type:Function]
@@ -85,7 +87,8 @@ def resolve_task_type(plugin_id: Optional[str]) -> TaskType:
# @PARAM: task_type (TaskType) - Canonical task type. # @PARAM: task_type (TaskType) - Canonical task type.
# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts. # @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.
def get_type_profile(task_type: TaskType) -> Dict[str, Any]: def get_type_profile(task_type: TaskType) -> Dict[str, Any]:
return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN]) with belief_scope("get_type_profile"):
return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])
# [/DEF:get_type_profile:Function] # [/DEF:get_type_profile:Function]
# [/DEF:backend.src.services.reports.type_profiles:Module] # [/DEF:backend.src.services.reports.type_profiles:Module]

View File

@@ -12,6 +12,8 @@
/** /**
* @TIER CRITICAL * @TIER CRITICAL
* @PURPOSE Displays detailed logs for a specific task inline or in a modal using TaskLogPanel. * @PURPOSE Displays detailed logs for a specific task inline or in a modal using TaskLogPanel.
* @PRE Needs a valid taskId to fetch logs for.
* @POST task logs are displayed and updated in real time.
* @UX_STATE Loading -> Shows spinner/text while fetching initial logs * @UX_STATE Loading -> Shows spinner/text while fetching initial logs
* @UX_STATE Streaming -> Displays logs with auto-scroll, real-time appending * @UX_STATE Streaming -> Displays logs with auto-scroll, real-time appending
* @UX_STATE Error -> Shows error message with recovery option * @UX_STATE Error -> Shows error message with recovery option
@@ -42,6 +44,9 @@
let shouldShow = $derived(inline || show); let shouldShow = $derived(inline || show);
// [DEF:handleRealTimeLogs:Action] // [DEF:handleRealTimeLogs:Action]
// @PURPOSE: Sync real-time logs to the current log list
// @PRE: None
// @POST: logs are updated with new real-time log entries
$effect(() => { $effect(() => {
if (realTimeLogs && realTimeLogs.length > 0) { if (realTimeLogs && realTimeLogs.length > 0) {
const lastLog = realTimeLogs[realTimeLogs.length - 1]; const lastLog = realTimeLogs[realTimeLogs.length - 1];
@@ -58,11 +63,20 @@
// [/DEF:handleRealTimeLogs:Action] // [/DEF:handleRealTimeLogs:Action]
// [DEF:fetchLogs:Function] // [DEF:fetchLogs:Function]
// @PURPOSE: Fetches logs for a given task ID
// @PRE: taskId is set
// @POST: logs are populated with API response
async function fetchLogs() { async function fetchLogs() {
if (!taskId) return; if (!taskId) return;
try { try {
console.log(`[TaskLogViewer][API][fetchLogs:STARTED] id=${taskId}`);
logs = await getTaskLogs(taskId); logs = await getTaskLogs(taskId);
console.log(`[TaskLogViewer][API][fetchLogs:SUCCESS] id=${taskId}`);
} catch (e) { } catch (e) {
console.error(
`[TaskLogViewer][API][fetchLogs:FAILED] id=${taskId}`,
e,
);
error = e.message; error = e.message;
} finally { } finally {
loading = false; loading = false;
@@ -70,13 +84,25 @@
} }
// [/DEF:fetchLogs:Function] // [/DEF:fetchLogs:Function]
// [DEF:handleFilterChange:Function]
// @PURPOSE: Updates filter conditions for the log viewer
// @PRE: event contains detail with source and level
// @POST: Log viewer filters updated
function handleFilterChange(event) { function handleFilterChange(event) {
console.log("[TaskLogViewer][UI][handleFilterChange:START]");
const { source, level } = event.detail; const { source, level } = event.detail;
} }
// [/DEF:handleFilterChange:Function]
// [DEF:handleRefresh:Function]
// @PURPOSE: Refreshes the logs by polling the API
// @PRE: None
// @POST: Logs refetched
function handleRefresh() { function handleRefresh() {
console.log("[TaskLogViewer][UI][handleRefresh:START]");
fetchLogs(); fetchLogs();
} }
// [/DEF:handleRefresh:Function]
$effect(() => { $effect(() => {
if (shouldShow && taskId) { if (shouldShow && taskId) {
@@ -104,6 +130,11 @@
</script> </script>
{#if shouldShow} {#if shouldShow}
<!-- [DEF:showInline:Component] -->
<!-- @PURPOSE: Shows inline logs -->
<!-- @LAYER: UI -->
<!-- @SEMANTICS: logs, inline -->
<!-- @TIER: STANDARD -->
{#if inline} {#if inline}
<div class="flex flex-col h-full w-full"> <div class="flex flex-col h-full w-full">
{#if loading && logs.length === 0} {#if loading && logs.length === 0}
@@ -136,7 +167,13 @@
/> />
{/if} {/if}
</div> </div>
<!-- [/DEF:showInline:Component] -->
{:else} {:else}
<!-- [DEF:showModal:Component] -->
<!-- @PURPOSE: Shows modal logs -->
<!-- @LAYER: UI -->
<!-- @SEMANTICS: logs, modal -->
<!-- @TIER: STANDARD -->
<div <div
class="fixed inset-0 z-50 overflow-y-auto" class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title" aria-labelledby="modal-title"
@@ -199,5 +236,6 @@
</div> </div>
{/if} {/if}
{/if} {/if}
// [/DEF:showModal:Component]
<!-- [/DEF:TaskLogViewer:Component] --> <!-- [/DEF:TaskLogViewer:Component] -->

View File

@@ -13,6 +13,7 @@ import { api } from '../api.js';
// @PRE: options is an object with optional report query fields. // @PRE: options is an object with optional report query fields.
// @POST: Returns URL query string without leading '?'. // @POST: Returns URL query string without leading '?'.
export function buildReportQueryString(options = {}) { export function buildReportQueryString(options = {}) {
console.log("[reports][api][buildReportQueryString:START]");
const params = new URLSearchParams(); const params = new URLSearchParams();
if (options.page != null) params.append('page', String(options.page)); if (options.page != null) params.append('page', String(options.page));
@@ -40,6 +41,7 @@ export function buildReportQueryString(options = {}) {
// @PRE: error may be Error/string/object. // @PRE: error may be Error/string/object.
// @POST: Returns structured error object. // @POST: Returns structured error object.
export function normalizeApiError(error) { export function normalizeApiError(error) {
console.log("[reports][api][normalizeApiError:START]");
const message = const message =
(error && typeof error.message === 'string' && error.message) || (error && typeof error.message === 'string' && error.message) ||
(typeof error === 'string' && error) || (typeof error === 'string' && error) ||
@@ -59,9 +61,13 @@ export function normalizeApiError(error) {
// @POST: Returns parsed payload or structured error for UI-state mapping. // @POST: Returns parsed payload or structured error for UI-state mapping.
export async function getReports(options = {}) { export async function getReports(options = {}) {
try { try {
console.log("[reports][api][getReports:STARTED]", options);
const query = buildReportQueryString(options); const query = buildReportQueryString(options);
return await api.fetchApi(`/reports${query ? `?${query}` : ''}`); const res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);
console.log("[reports][api][getReports:SUCCESS]", res);
return res;
} catch (error) { } catch (error) {
console.error("[reports][api][getReports:FAILED]", error);
throw normalizeApiError(error); throw normalizeApiError(error);
} }
} }
@@ -73,8 +79,12 @@ export async function getReports(options = {}) {
// @POST: Returns parsed detail payload or structured error object. // @POST: Returns parsed detail payload or structured error object.
export async function getReportDetail(reportId) { export async function getReportDetail(reportId) {
try { try {
return await api.fetchApi(`/reports/${reportId}`); console.log(`[reports][api][getReportDetail:STARTED] id=${reportId}`);
const res = await api.fetchApi(`/reports/${reportId}`);
console.log(`[reports][api][getReportDetail:SUCCESS] id=${reportId}`);
return res;
} catch (error) { } catch (error) {
console.error(`[reports][api][getReportDetail:FAILED] id=${reportId}`, error);
throw normalizeApiError(error); throw normalizeApiError(error);
} }
} }

View File

@@ -23,35 +23,35 @@
* @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id} * @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id}
*/ */
import { onMount } from 'svelte'; import { onMount } from "svelte";
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import { t } from '$lib/i18n'; import { t } from "$lib/i18n";
import Icon from '$lib/ui/Icon.svelte'; import Icon from "$lib/ui/Icon.svelte";
import { openDrawerForTask } from '$lib/stores/taskDrawer.js'; import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
import { import {
assistantChatStore, assistantChatStore,
closeAssistantChat, closeAssistantChat,
setAssistantConversationId, setAssistantConversationId,
} from '$lib/stores/assistantChat.js'; } from "$lib/stores/assistantChat.js";
import { import {
sendAssistantMessage, sendAssistantMessage,
confirmAssistantOperation, confirmAssistantOperation,
cancelAssistantOperation, cancelAssistantOperation,
getAssistantHistory, getAssistantHistory,
getAssistantConversations, getAssistantConversations,
} from '$lib/api/assistant.js'; } from "$lib/api/assistant.js";
const HISTORY_PAGE_SIZE = 30; const HISTORY_PAGE_SIZE = 30;
const CONVERSATIONS_PAGE_SIZE = 20; const CONVERSATIONS_PAGE_SIZE = 20;
let input = ''; let input = "";
let loading = false; let loading = false;
let loadingHistory = false; let loadingHistory = false;
let loadingMoreHistory = false; let loadingMoreHistory = false;
let loadingConversations = false; let loadingConversations = false;
let messages = []; let messages = [];
let conversations = []; let conversations = [];
let conversationFilter = 'active'; let conversationFilter = "active";
let activeConversationsTotal = 0; let activeConversationsTotal = 0;
let archivedConversationsTotal = 0; let archivedConversationsTotal = 0;
let historyPage = 1; let historyPage = 1;
@@ -77,7 +77,12 @@
const requestVersion = ++historyLoadVersion; const requestVersion = ++historyLoadVersion;
loadingHistory = true; loadingHistory = true;
try { try {
const history = await getAssistantHistory(1, HISTORY_PAGE_SIZE, targetConversationId, true); const history = await getAssistantHistory(
1,
HISTORY_PAGE_SIZE,
targetConversationId,
true,
);
if (requestVersion !== historyLoadVersion) { if (requestVersion !== historyLoadVersion) {
return; return;
} }
@@ -87,13 +92,21 @@
})); }));
historyPage = 1; historyPage = 1;
historyHasNext = Boolean(history.has_next); historyHasNext = Boolean(history.has_next);
if (!targetConversationId && history.conversation_id && history.conversation_id !== conversationId) { if (
!targetConversationId &&
history.conversation_id &&
history.conversation_id !== conversationId
) {
setAssistantConversationId(history.conversation_id); setAssistantConversationId(history.conversation_id);
} }
initialized = true; initialized = true;
console.log('[AssistantChatPanel][Coherence:OK] History loaded'); // prettier-ignore
console.log("[AssistantChatPanel][history][loadHistory:SUCCESS] History loaded");
} catch (err) { } catch (err) {
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load history', err); console.error(
"[AssistantChatPanel][history][loadHistory:FAILED] Failed to load history",
err,
);
} finally { } finally {
loadingHistory = false; loadingHistory = false;
} }
@@ -111,23 +124,30 @@
loadingConversations = true; loadingConversations = true;
try { try {
const page = reset ? 1 : conversationsPage + 1; const page = reset ? 1 : conversationsPage + 1;
const includeArchived = conversationFilter === 'archived'; const includeArchived = conversationFilter === "archived";
const archivedOnly = conversationFilter === 'archived'; const archivedOnly = conversationFilter === "archived";
const response = await getAssistantConversations( const response = await getAssistantConversations(
page, page,
CONVERSATIONS_PAGE_SIZE, CONVERSATIONS_PAGE_SIZE,
includeArchived, includeArchived,
'', "",
archivedOnly, archivedOnly,
); );
const rows = response.items || []; const rows = response.items || [];
conversations = reset ? rows : [...conversations, ...rows]; conversations = reset ? rows : [...conversations, ...rows];
conversationsPage = page; conversationsPage = page;
conversationsHasNext = Boolean(response.has_next); conversationsHasNext = Boolean(response.has_next);
activeConversationsTotal = response.active_total ?? activeConversationsTotal; activeConversationsTotal =
archivedConversationsTotal = response.archived_total ?? archivedConversationsTotal; response.active_total ?? activeConversationsTotal;
archivedConversationsTotal =
response.archived_total ?? archivedConversationsTotal;
// prettier-ignore
console.log("[AssistantChatPanel][conversations][loadConversations:SUCCESS]");
} catch (err) { } catch (err) {
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load conversations', err); console.error(
"[AssistantChatPanel][conversations][loadConversations:FAILED]",
err,
);
} finally { } finally {
loadingConversations = false; loadingConversations = false;
} }
@@ -141,11 +161,22 @@
* @POST: Older messages are prepended while preserving order. * @POST: Older messages are prepended while preserving order.
*/ */
async function loadOlderMessages() { async function loadOlderMessages() {
if (loadingMoreHistory || loadingHistory || !historyHasNext || !conversationId) return; if (
loadingMoreHistory ||
loadingHistory ||
!historyHasNext ||
!conversationId
)
return;
loadingMoreHistory = true; loadingMoreHistory = true;
try { try {
const nextPage = historyPage + 1; const nextPage = historyPage + 1;
const history = await getAssistantHistory(nextPage, HISTORY_PAGE_SIZE, conversationId, true); const history = await getAssistantHistory(
nextPage,
HISTORY_PAGE_SIZE,
conversationId,
true,
);
const chunk = (history.items || []).map((msg) => ({ const chunk = (history.items || []).map((msg) => ({
...msg, ...msg,
actions: msg.actions || msg.metadata?.actions || [], actions: msg.actions || msg.metadata?.actions || [],
@@ -155,8 +186,12 @@
messages = [...uniqueChunk, ...messages]; messages = [...uniqueChunk, ...messages];
historyPage = nextPage; historyPage = nextPage;
historyHasNext = Boolean(history.has_next); historyHasNext = Boolean(history.has_next);
console.log("[AssistantChatPanel][history][loadOlderMessages:SUCCESS]");
} catch (err) { } catch (err) {
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load older messages', err); console.error(
"[AssistantChatPanel][history][loadOlderMessages:FAILED]",
err,
);
} finally { } finally {
loadingMoreHistory = false; loadingMoreHistory = false;
} }
@@ -170,7 +205,9 @@
$: if (isOpen && initialized && conversationId) { $: if (isOpen && initialized && conversationId) {
// Re-load only when user switched to another conversation. // Re-load only when user switched to another conversation.
const currentFirstConversationId = messages.length ? messages[0].conversation_id : conversationId; const currentFirstConversationId = messages.length
? messages[0].conversation_id
: conversationId;
if (currentFirstConversationId !== conversationId) { if (currentFirstConversationId !== conversationId) {
loadHistory(); loadHistory();
} }
@@ -183,11 +220,12 @@
* @POST: user message appears at the end of messages list. * @POST: user message appears at the end of messages list.
*/ */
function appendLocalUserMessage(text) { function appendLocalUserMessage(text) {
console.log("[AssistantChatPanel][message][appendLocalUserMessage][START]");
messages = [ messages = [
...messages, ...messages,
{ {
message_id: `local-${Date.now()}`, message_id: `local-${Date.now()}`,
role: 'user', role: "user",
text, text,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}, },
@@ -202,11 +240,13 @@
* @POST: assistant message appended with state/task/actions metadata. * @POST: assistant message appended with state/task/actions metadata.
*/ */
function appendAssistantResponse(response) { function appendAssistantResponse(response) {
// prettier-ignore
console.log("[AssistantChatPanel][message][appendAssistantResponse][START]");
messages = [ messages = [
...messages, ...messages,
{ {
message_id: response.response_id, message_id: response.response_id,
role: 'assistant', role: "assistant",
text: response.text, text: response.text,
state: response.state, state: response.state,
task_id: response.task_id || null, task_id: response.task_id || null,
@@ -220,12 +260,12 @@
function buildConversationTitle(conversation) { function buildConversationTitle(conversation) {
if (conversation?.title?.trim()) return conversation.title.trim(); if (conversation?.title?.trim()) return conversation.title.trim();
if (!conversation?.conversation_id) return 'Conversation'; if (!conversation?.conversation_id) return "Conversation";
return `Conversation ${conversation.conversation_id.slice(0, 8)}`; return `Conversation ${conversation.conversation_id.slice(0, 8)}`;
} }
function setConversationFilter(filter) { function setConversationFilter(filter) {
if (filter !== 'active' && filter !== 'archived') return; if (filter !== "active" && filter !== "archived") return;
if (conversationFilter === filter) return; if (conversationFilter === filter) return;
conversationFilter = filter; conversationFilter = filter;
conversations = []; conversations = [];
@@ -235,9 +275,9 @@
} }
function formatConversationTime(iso) { function formatConversationTime(iso) {
if (!iso) return ''; if (!iso) return "";
const dt = new Date(iso); const dt = new Date(iso);
if (Number.isNaN(dt.getTime())) return ''; if (Number.isNaN(dt.getTime())) return "";
return dt.toLocaleString(); return dt.toLocaleString();
} }
@@ -249,11 +289,12 @@
* @SIDE_EFFECT: Triggers backend command execution pipeline. * @SIDE_EFFECT: Triggers backend command execution pipeline.
*/ */
async function handleSend() { async function handleSend() {
console.log("[AssistantChatPanel][message][handleSend][START]");
const text = input.trim(); const text = input.trim();
if (!text || loading) return; if (!text || loading) return;
appendLocalUserMessage(text); appendLocalUserMessage(text);
input = ''; input = "";
loading = true; loading = true;
try { try {
@@ -271,8 +312,8 @@
} catch (err) { } catch (err) {
appendAssistantResponse({ appendAssistantResponse({
response_id: `error-${Date.now()}`, response_id: `error-${Date.now()}`,
text: err.message || 'Assistant request failed', text: err.message || "Assistant request failed",
state: 'failed', state: "failed",
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
actions: [], actions: [],
}); });
@@ -289,6 +330,8 @@
* @POST: conversationId updated and history reloaded. * @POST: conversationId updated and history reloaded.
*/ */
async function selectConversation(conversation) { async function selectConversation(conversation) {
// prettier-ignore
console.log("[AssistantChatPanel][conversation][selectConversation][START]");
if (!conversation?.conversation_id) return; if (!conversation?.conversation_id) return;
if (conversation.conversation_id === conversationId) return; if (conversation.conversation_id === conversationId) return;
// Invalidate any in-flight history request to avoid stale conversation overwrite. // Invalidate any in-flight history request to avoid stale conversation overwrite.
@@ -308,8 +351,10 @@
* @POST: Messages reset and new conversation id bound. * @POST: Messages reset and new conversation id bound.
*/ */
function startNewConversation() { function startNewConversation() {
// prettier-ignore
console.log("[AssistantChatPanel][conversation][startNewConversation][START]");
const newId = const newId =
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID() ? crypto.randomUUID()
: `conv-${Date.now()}`; : `conv-${Date.now()}`;
setAssistantConversationId(newId); setAssistantConversationId(newId);
@@ -328,32 +373,37 @@
* @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints. * @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints.
*/ */
async function handleAction(action, message) { async function handleAction(action, message) {
console.log("[AssistantChatPanel][action][handleAction][START]");
try { try {
if (action.type === 'open_task' && action.target) { if (action.type === "open_task" && action.target) {
openDrawerForTask(action.target); openDrawerForTask(action.target);
return; return;
} }
if (action.type === 'open_reports') { if (action.type === "open_reports") {
goto('/reports'); goto("/reports");
return; return;
} }
if (action.type === 'confirm' && message.confirmation_id) { if (action.type === "confirm" && message.confirmation_id) {
const response = await confirmAssistantOperation(message.confirmation_id); const response = await confirmAssistantOperation(
message.confirmation_id,
);
appendAssistantResponse(response); appendAssistantResponse(response);
return; return;
} }
if (action.type === 'cancel' && message.confirmation_id) { if (action.type === "cancel" && message.confirmation_id) {
const response = await cancelAssistantOperation(message.confirmation_id); const response = await cancelAssistantOperation(
message.confirmation_id,
);
appendAssistantResponse(response); appendAssistantResponse(response);
} }
} catch (err) { } catch (err) {
appendAssistantResponse({ appendAssistantResponse({
response_id: `action-error-${Date.now()}`, response_id: `action-error-${Date.now()}`,
text: err.message || 'Action failed', text: err.message || "Action failed",
state: 'failed', state: "failed",
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
actions: [], actions: [],
}); });
@@ -368,7 +418,8 @@
* @POST: handleSend is invoked when Enter is pressed without shift modifier. * @POST: handleSend is invoked when Enter is pressed without shift modifier.
*/ */
function handleKeydown(event) { function handleKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) { console.log("[AssistantChatPanel][input][handleKeydown][START]");
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
handleSend(); handleSend();
} }
@@ -382,12 +433,17 @@
* @POST: Tailwind class string returned for badge rendering. * @POST: Tailwind class string returned for badge rendering.
*/ */
function stateClass(state) { function stateClass(state) {
if (state === 'started') return 'bg-sky-100 text-sky-700 border-sky-200'; console.log("[AssistantChatPanel][ui][stateClass][START]");
if (state === 'success') return 'bg-emerald-100 text-emerald-700 border-emerald-200'; if (state === "started") return "bg-sky-100 text-sky-700 border-sky-200";
if (state === 'needs_confirmation') return 'bg-amber-100 text-amber-700 border-amber-200'; if (state === "success")
if (state === 'denied' || state === 'failed') return 'bg-rose-100 text-rose-700 border-rose-200'; return "bg-emerald-100 text-emerald-700 border-emerald-200";
if (state === 'needs_clarification') return 'bg-violet-100 text-violet-700 border-violet-200'; if (state === "needs_confirmation")
return 'bg-slate-100 text-slate-700 border-slate-200'; return "bg-amber-100 text-amber-700 border-amber-200";
if (state === "denied" || state === "failed")
return "bg-rose-100 text-rose-700 border-rose-200";
if (state === "needs_clarification")
return "bg-violet-100 text-violet-700 border-violet-200";
return "bg-slate-100 text-slate-700 border-slate-200";
} }
// [/DEF:stateClass:Function] // [/DEF:stateClass:Function]
@@ -398,8 +454,9 @@
* @POST: loadOlderMessages called when boundary and more pages available. * @POST: loadOlderMessages called when boundary and more pages available.
*/ */
function handleHistoryScroll(event) { function handleHistoryScroll(event) {
console.log("[AssistantChatPanel][scroll][handleHistoryScroll][START]");
const el = event.currentTarget; const el = event.currentTarget;
if (!el || typeof el.scrollTop !== 'number') return; if (!el || typeof el.scrollTop !== "number") return;
if (el.scrollTop <= 16) { if (el.scrollTop <= 16) {
loadOlderMessages(); loadOlderMessages();
} }
@@ -412,18 +469,28 @@
</script> </script>
{#if isOpen} {#if isOpen}
<div class="fixed inset-0 z-[70] bg-slate-900/30" on:click={closeAssistantChat} aria-hidden="true"></div> <div
class="fixed inset-0 z-[70] bg-slate-900/30"
on:click={closeAssistantChat}
aria-hidden="true"
></div>
<aside class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl"> <aside
<div class="flex h-14 items-center justify-between border-b border-slate-200 px-4"> class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl"
>
<div
class="flex h-14 items-center justify-between border-b border-slate-200 px-4"
>
<div class="flex items-center gap-2 text-slate-800"> <div class="flex items-center gap-2 text-slate-800">
<Icon name="clipboard" size={18} /> <Icon name="clipboard" size={18} />
<h2 class="text-sm font-semibold">{$t.assistant?.title || 'AI Assistant'}</h2> <h2 class="text-sm font-semibold">
{$t.assistant?.title || "AI Assistant"}
</h2>
</div> </div>
<button <button
class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900" class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
on:click={closeAssistantChat} on:click={closeAssistantChat}
aria-label={$t.assistant?.close || 'Close assistant'} aria-label={$t.assistant?.close || "Close assistant"}
> >
<Icon name="close" size={18} /> <Icon name="close" size={18} />
</button> </button>
@@ -432,7 +499,10 @@
<div class="flex h-[calc(100%-56px)] flex-col"> <div class="flex h-[calc(100%-56px)] flex-col">
<div class="border-b border-slate-200 px-3 py-2"> <div class="border-b border-slate-200 px-3 py-2">
<div class="mb-2 flex items-center justify-between"> <div class="mb-2 flex items-center justify-between">
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Conversations</span> <span
class="text-xs font-semibold uppercase tracking-wide text-slate-500"
>Conversations</span
>
<button <button
class="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100" class="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100"
on:click={startNewConversation} on:click={startNewConversation}
@@ -442,14 +512,20 @@
</div> </div>
<div class="mb-2 flex items-center gap-1"> <div class="mb-2 flex items-center gap-1">
<button <button
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter === 'active' ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}" class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
on:click={() => setConversationFilter('active')} 'active'
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
on:click={() => setConversationFilter("active")}
> >
Active ({activeConversationsTotal}) Active ({activeConversationsTotal})
</button> </button>
<button <button
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter === 'archived' ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}" class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
on:click={() => setConversationFilter('archived')} 'archived'
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
on:click={() => setConversationFilter("archived")}
> >
Archived ({archivedConversationsTotal}) Archived ({archivedConversationsTotal})
</button> </button>
@@ -457,16 +533,27 @@
<div class="flex gap-2 overflow-x-auto pb-1"> <div class="flex gap-2 overflow-x-auto pb-1">
{#each conversations as convo (convo.conversation_id)} {#each conversations as convo (convo.conversation_id)}
<button <button
class="min-w-[140px] max-w-[220px] rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id === conversationId ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}" class="min-w-[140px] max-w-[220px] rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id ===
conversationId
? 'border-sky-300 bg-sky-50 text-sky-900'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
on:click={() => selectConversation(convo)} on:click={() => selectConversation(convo)}
title={formatConversationTime(convo.updated_at)} title={formatConversationTime(convo.updated_at)}
> >
<div class="truncate font-semibold">{buildConversationTitle(convo)}</div> <div class="truncate font-semibold">
<div class="truncate text-[10px] text-slate-500">{convo.last_message || ''}</div> {buildConversationTitle(convo)}
</div>
<div class="truncate text-[10px] text-slate-500">
{convo.last_message || ""}
</div>
</button> </button>
{/each} {/each}
{#if loadingConversations} {#if loadingConversations}
<div class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs text-slate-500">...</div> <div
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs text-slate-500"
>
...
</div>
{/if} {/if}
{#if conversationsHasNext} {#if conversationsHasNext}
<button <button
@@ -479,15 +566,29 @@
</div> </div>
</div> </div>
<div class="flex-1 space-y-3 overflow-y-auto p-4" bind:this={historyViewport} on:scroll={handleHistoryScroll}> <div
class="flex-1 space-y-3 overflow-y-auto p-4"
bind:this={historyViewport}
on:scroll={handleHistoryScroll}
>
{#if loadingMoreHistory} {#if loadingMoreHistory}
<div class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center text-xs text-slate-500">Loading older messages...</div> <div
class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center text-xs text-slate-500"
>
Loading older messages...
</div>
{/if} {/if}
{#if loadingHistory} {#if loadingHistory}
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">{$t.assistant?.loading_history || 'Loading history...'}</div> <div
class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
>
{$t.assistant?.loading_history || "Loading history..."}
</div>
{:else if messages.length === 0} {:else if messages.length === 0}
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"> <div
{$t.assistant?.try_commands || 'Try commands:'} class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
>
{$t.assistant?.try_commands || "Try commands:"}
<div class="mt-2 space-y-1 text-xs"> <div class="mt-2 space-y-1 text-xs">
<div>• сделай ветку feature/new-dashboard для дашборда 42</div> <div>• сделай ветку feature/new-dashboard для дашборда 42</div>
<div>• запусти миграцию с dev на prod для дашборда 42</div> <div>• запусти миграцию с dev на prod для дашборда 42</div>
@@ -497,29 +598,44 @@
{/if} {/if}
{#each messages as message (message.message_id)} {#each messages as message (message.message_id)}
<div class={message.role === 'user' ? 'ml-8' : 'mr-8'}> <div class={message.role === "user" ? "ml-8" : "mr-8"}>
<div class="rounded-xl border p-3 {message.role === 'user' ? 'border-sky-200 bg-sky-50' : 'border-slate-200 bg-white'}"> <div
class="rounded-xl border p-3 {message.role === 'user'
? 'border-sky-200 bg-sky-50'
: 'border-slate-200 bg-white'}"
>
<div class="mb-1 flex items-center justify-between gap-2"> <div class="mb-1 flex items-center justify-between gap-2">
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"> <span
{message.role === 'user' ? 'You' : 'Assistant'} class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
>
{message.role === "user" ? "You" : "Assistant"}
</span> </span>
{#if message.state} {#if message.state}
<span class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(message.state)}"> <span
class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(
message.state,
)}"
>
{$t.assistant?.states?.[message.state] || message.state} {$t.assistant?.states?.[message.state] || message.state}
</span> </span>
{/if} {/if}
</div> </div>
<div class="whitespace-pre-wrap text-sm text-slate-800">{message.text}</div> <div class="whitespace-pre-wrap text-sm text-slate-800">
{message.text}
</div>
{#if message.task_id} {#if message.task_id}
<div class="mt-2 flex items-center gap-2"> <div class="mt-2 flex items-center gap-2">
<span class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700">task_id: {message.task_id}</span> <span
class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700"
>task_id: {message.task_id}</span
>
<button <button
class="text-xs font-medium text-sky-700 hover:text-sky-900" class="text-xs font-medium text-sky-700 hover:text-sky-900"
on:click={() => openDrawerForTask(message.task_id)} on:click={() => openDrawerForTask(message.task_id)}
> >
{$t.assistant?.open_task_drawer || 'Open Task Drawer'} {$t.assistant?.open_task_drawer || "Open Task Drawer"}
</button> </button>
</div> </div>
{/if} {/if}
@@ -544,12 +660,14 @@
<div class="mr-8"> <div class="mr-8">
<div class="rounded-xl border border-slate-200 bg-white p-3"> <div class="rounded-xl border border-slate-200 bg-white p-3">
<div class="mb-1 flex items-center justify-between gap-2"> <div class="mb-1 flex items-center justify-between gap-2">
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"> <span
class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
>
Assistant Assistant
</span> </span>
</div> </div>
<div class="flex items-center gap-2 text-sm text-slate-700"> <div class="flex items-center gap-2 text-sm text-slate-700">
<span>{$t.assistant?.thinking || 'Думаю'}</span> <span>{$t.assistant?.thinking || "Думаю"}</span>
<span class="thinking-dots" aria-hidden="true"> <span class="thinking-dots" aria-hidden="true">
<span></span><span></span><span></span> <span></span><span></span><span></span>
</span> </span>
@@ -564,7 +682,7 @@
<textarea <textarea
bind:value={input} bind:value={input}
rows="2" rows="2"
placeholder={$t.assistant?.input_placeholder || 'Type a command...'} placeholder={$t.assistant?.input_placeholder || "Type a command..."}
class="min-h-[52px] w-full resize-y rounded-lg border border-slate-300 px-3 py-2 text-sm outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-100" class="min-h-[52px] w-full resize-y rounded-lg border border-slate-300 px-3 py-2 text-sm outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
on:keydown={handleKeydown} on:keydown={handleKeydown}
></textarea> ></textarea>
@@ -573,7 +691,7 @@
on:click={handleSend} on:click={handleSend}
disabled={loading || !input.trim()} disabled={loading || !input.trim()}
> >
{loading ? '...' : ($t.assistant?.send || 'Send')} {loading ? "..." : $t.assistant?.send || "Send"}
</button> </button>
</div> </div>
</div> </div>
@@ -581,6 +699,8 @@
</aside> </aside>
{/if} {/if}
<!-- [/DEF:AssistantChatPanel:Component] -->
<style> <style>
.thinking-dots { .thinking-dots {
display: inline-flex; display: inline-flex;
@@ -618,4 +738,3 @@
} }
} }
</style> </style>
<!-- [/DEF:AssistantChatPanel:Component] -->

View File

@@ -55,7 +55,7 @@
// Close drawer // Close drawer
function handleClose() { function handleClose() {
console.log("[TaskDrawer][Action] Close drawer"); console.log("[TaskDrawer][ui][Close_drawer]");
closeDrawer(); closeDrawer();
} }
@@ -98,7 +98,7 @@
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log("[TaskDrawer][WebSocket] Received message:", data); console.log(`[TaskDrawer][WebSocket][Message_Received] ${data.message}`);
realTimeLogs = [...realTimeLogs, data]; realTimeLogs = [...realTimeLogs, data];
@@ -118,28 +118,40 @@
}; };
} }
// Disconnect WebSocket // [DEF:disconnectWebSocket:Function]
/**
* @PURPOSE: Disconnects the active WebSocket connection
* @PRE: ws may or may not be initialized
* @POST: ws is closed and set to null
* @TIER: STANDARD
*/
function disconnectWebSocket() { function disconnectWebSocket() {
console.log("[TaskDrawer][WebSocket][disconnectWebSocket:START]");
if (ws) { if (ws) {
ws.close(); ws.close();
ws = null; ws = null;
} }
} }
// [/DEF:disconnectWebSocket:Function]
// [DEF:loadRecentTasks:Function] // [DEF:loadRecentTasks:Function]
/** /**
* @PURPOSE: Load recent tasks for list mode display * @PURPOSE: Load recent tasks for list mode display
* @PRE: User is on task drawer or api is ready.
* @POST: recentTasks array populated with task list * @POST: recentTasks array populated with task list
*/ */
async function loadRecentTasks() { async function loadRecentTasks() {
loadingTasks = true; loadingTasks = true;
try { try {
console.log("[TaskDrawer][API][loadRecentTasks:STARTED]");
// API returns List[Task] directly, not {tasks: [...]} // API returns List[Task] directly, not {tasks: [...]}
const response = await api.getTasks(); const response = await api.getTasks();
recentTasks = Array.isArray(response) ? response : (response.tasks || []); recentTasks = Array.isArray(response) ? response : response.tasks || [];
console.log("[TaskDrawer][Action] Loaded recent tasks:", recentTasks.length); console.log(
`[TaskDrawer][API][loadRecentTasks:SUCCESS] loaded ${recentTasks.length} tasks`,
);
} catch (err) { } catch (err) {
console.error("[TaskDrawer][Coherence:Failed] Failed to load tasks:", err); console.error("[TaskDrawer][API][loadRecentTasks:FAILED]", err);
recentTasks = []; recentTasks = [];
} finally { } finally {
loadingTasks = false; loadingTasks = false;
@@ -150,11 +162,14 @@
// [DEF:selectTask:Function] // [DEF:selectTask:Function]
/** /**
* @PURPOSE: Select a task from list to view details * @PURPOSE: Select a task from list to view details
* @PRE: task is a valid task object
* @POST: drawer state updated to show task details
*/ */
function selectTask(task) { function selectTask(task) {
taskDrawerStore.update(state => ({ console.log("[TaskDrawer][UI][selectTask:START]");
taskDrawerStore.update((state) => ({
...state, ...state,
activeTaskId: task.id activeTaskId: task.id,
})); }));
} }
// [/DEF:selectTask:Function] // [/DEF:selectTask:Function]
@@ -162,14 +177,19 @@
// [DEF:goBackToList:Function] // [DEF:goBackToList:Function]
/** /**
* @PURPOSE: Return to task list view from task details * @PURPOSE: Return to task list view from task details
* @PRE: Drawer is open and activeTaskId is set
* @POST: Drawer switches to list view and reloads tasks
* @TIER: STANDARD
*/ */
function goBackToList() { function goBackToList() {
taskDrawerStore.update(state => ({ console.log("[TaskDrawer][UI][goBackToList:START]");
taskDrawerStore.update((state) => ({
...state, ...state,
activeTaskId: null activeTaskId: null,
})); }));
// Reload the task list // Reload the task list
loadRecentTasks(); loadRecentTasks();
console.log("[TaskDrawer][UI][goBackToList:SUCCESS]");
} }
// [/DEF:goBackToList:Function] // [/DEF:goBackToList:Function]
@@ -202,100 +222,145 @@
aria-modal="false" aria-modal="false"
aria-label={$t.tasks?.drawer || "Task drawer"} aria-label={$t.tasks?.drawer || "Task drawer"}
> >
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5"> <div
<div class="flex items-center gap-2.5"> class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5"
{#if !activeTaskId && recentTasks.length > 0} >
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400"> <div class="flex items-center gap-2.5">
<Icon name="list" size={16} strokeWidth={2} /> {#if !activeTaskId && recentTasks.length > 0}
</span> <span
{:else if activeTaskId} class="flex items-center justify-center p-1.5 mr-1 text-cyan-400"
<button
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
on:click={goBackToList}
aria-label={$t.tasks?.back_to_list || "Back to task list"}
>
<Icon name="back" size={16} strokeWidth={2} />
</button>
{/if}
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : ($t.tasks?.recent || 'Recent Tasks')}
</h2>
{#if shortTaskId}
<span class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded">{shortTaskId}</span>
{/if}
{#if taskStatus}
<span class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() === 'running' ? 'text-cyan-400 bg-cyan-400/10 border border-cyan-400/20' : taskStatus.toLowerCase() === 'success' ? 'text-green-400 bg-green-400/10 border border-green-400/20' : 'text-red-400 bg-red-400/10 border border-red-400/20'}"
>{taskStatus}</span
>
{/if}
</div>
<div class="flex items-center gap-2">
<button
class="rounded-md border border-slate-300 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100"
on:click={goToReportsPage}
> >
{$t.nav?.reports || "Reports"} <Icon name="list" size={16} strokeWidth={2} />
</button> </span>
{:else if activeTaskId}
<button <button
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800" class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
on:click={handleClose} on:click={goBackToList}
aria-label={$t.tasks?.close_drawer || "Close drawer"} aria-label={$t.tasks?.back_to_list || "Back to task list"}
> >
<Icon name="close" size={18} strokeWidth={2} /> <Icon name="back" size={16} strokeWidth={2} />
</button> </button>
</div> {/if}
</div> <h2 class="text-sm font-semibold tracking-tight text-slate-900">
{activeTaskId
<!-- Content --> ? $t.tasks?.details_logs || "Task Details & Logs"
<div class="flex-1 overflow-hidden flex flex-col"> : $t.tasks?.recent || "Recent Tasks"}
{#if activeTaskId} </h2>
<TaskLogViewer {#if shortTaskId}
inline={true} <span
taskId={activeTaskId} class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded"
{taskStatus} >{shortTaskId}</span
{realTimeLogs} >
/> {/if}
{:else if loadingTasks} {#if taskStatus}
<div class="flex flex-col items-center justify-center p-12 text-slate-500"> <span
<div class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"></div> class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() ===
<p>{$t.tasks?.loading || 'Loading tasks...'}</p> 'running'
</div> ? 'text-cyan-400 bg-cyan-400/10 border border-cyan-400/20'
{:else if recentTasks.length > 0} : taskStatus.toLowerCase() === 'success'
<div class="p-4"> ? 'text-green-400 bg-green-400/10 border border-green-400/20'
<h3 class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800">{$t.tasks?.recent || 'Recent Tasks'}</h3> : 'text-red-400 bg-red-400/10 border border-red-400/20'}"
{#each recentTasks as task} >{taskStatus}</span
<button >
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
on:click={() => selectTask(task)}
>
<span class="font-mono text-xs text-slate-500">{task.id?.substring(0, 8) || ($t.common?.not_available || 'N/A')}...</span>
<span class="flex-1 text-sm text-slate-100 font-medium">{task.plugin_id || ($t.common?.unknown || 'Unknown')}</span>
<span class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() === 'running' || task.status?.toLowerCase() === 'pending' ? 'bg-cyan-500/15 text-cyan-400' : task.status?.toLowerCase() === 'completed' || task.status?.toLowerCase() === 'success' ? 'bg-green-500/15 text-green-400' : task.status?.toLowerCase() === 'failed' || task.status?.toLowerCase() === 'error' ? 'bg-red-500/15 text-red-400' : 'bg-slate-500/15 text-slate-400'}">{task.status || ($t.common?.unknown || 'UNKNOWN')}</span>
</button>
{/each}
</div>
{:else}
<div class="flex flex-col items-center justify-center h-full text-slate-500">
<Icon
name="clipboard"
size={48}
strokeWidth={1.6}
className="mb-3 text-slate-700"
/>
<p>{$t.tasks?.select_task || 'No recent tasks'}</p>
</div>
{/if} {/if}
</div> </div>
<div class="flex items-center gap-2">
<!-- Footer --> <button
<div class="flex items-center gap-2 justify-center px-4 py-2.5 border-t border-slate-800 bg-slate-900"> class="rounded-md border border-slate-300 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100"
<div class="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></div> on:click={goToReportsPage}
<p class="text-xs text-slate-500"> >
{$t.tasks?.footer_text || 'Task continues running in background'} {$t.nav?.reports || "Reports"}
</p> </button>
<button
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
on:click={handleClose}
aria-label={$t.tasks?.close_drawer || "Close drawer"}
>
<Icon name="close" size={18} strokeWidth={2} />
</button>
</div> </div>
</aside> </div>
{/if}
<!-- [/DEF:TaskDrawer:Component] --> <!-- Content -->
<div class="flex-1 overflow-hidden flex flex-col">
{#if activeTaskId}
<TaskLogViewer
inline={true}
taskId={activeTaskId}
{taskStatus}
{realTimeLogs}
/>
{:else if loadingTasks}
<div
class="flex flex-col items-center justify-center p-12 text-slate-500"
>
<div
class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"
></div>
<p>{$t.tasks?.loading || "Loading tasks..."}</p>
</div>
{:else if recentTasks.length > 0}
<div class="p-4">
<h3
class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800"
>
{$t.tasks?.recent || "Recent Tasks"}
</h3>
{#each recentTasks as task}
<button
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
on:click={() => selectTask(task)}
>
<span class="font-mono text-xs text-slate-500"
>{task.id?.substring(0, 8) ||
$t.common?.not_available ||
"N/A"}...</span
>
<span class="flex-1 text-sm text-slate-100 font-medium"
>{task.plugin_id || $t.common?.unknown || "Unknown"}</span
>
<span
class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() ===
'running' || task.status?.toLowerCase() === 'pending'
? 'bg-cyan-500/15 text-cyan-400'
: task.status?.toLowerCase() === 'completed' ||
task.status?.toLowerCase() === 'success'
? 'bg-green-500/15 text-green-400'
: task.status?.toLowerCase() === 'failed' ||
task.status?.toLowerCase() === 'error'
? 'bg-red-500/15 text-red-400'
: 'bg-slate-500/15 text-slate-400'}"
>{task.status || $t.common?.unknown || "UNKNOWN"}</span
>
</button>
{/each}
</div>
{:else}
<div
class="flex flex-col items-center justify-center h-full text-slate-500"
>
<Icon
name="clipboard"
size={48}
strokeWidth={1.6}
className="mb-3 text-slate-700"
/>
<p>{$t.tasks?.select_task || "No recent tasks"}</p>
</div>
{/if}
</div>
<!-- Footer -->
<div
class="flex items-center gap-2 justify-center px-4 py-2.5 border-t border-slate-800 bg-slate-900"
>
<div class="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></div>
<p class="text-xs text-slate-500">
{$t.tasks?.footer_text || "Task continues running in background"}
</p>
</div>
</aside>
<!-- [/DEF:TaskDrawer:Component] -->
{/if}
```

View File

@@ -52,6 +52,8 @@ export const REPORT_TYPE_PROFILES = {
// @POST: Returns one profile object. // @POST: Returns one profile object.
export function getReportTypeProfile(taskType) { export function getReportTypeProfile(taskType) {
const key = typeof taskType === 'string' ? taskType : 'unknown'; const key = typeof taskType === 'string' ? taskType : 'unknown';
console.log("[reports][ui][getReportTypeProfile][STATE:START]");
console.log("[reports][ui][getReportTypeProfile] Resolved type '" + taskType + "' to profile '" + key + "'");
return REPORT_TYPE_PROFILES[key] || REPORT_TYPE_PROFILES.unknown; return REPORT_TYPE_PROFILES[key] || REPORT_TYPE_PROFILES.unknown;
} }
// [/DEF:getReportTypeProfile:Function] // [/DEF:getReportTypeProfile:Function]

123
gen_map_module.json Normal file
View File

@@ -0,0 +1,123 @@
{
"name": "generate_semantic_map",
"type": "Module",
"tier": "STANDARD",
"start_line": 1,
"end_line": 53,
"tags": {
"PURPOSE": "Scans the codebase to generate a Semantic Map, Module Map, and Compliance Report based on the System Standard.",
"PRE": "Valid directory containing code to scan.",
"POST": "Files map.json, .ai/PROJECT_MAP.md, .ai/MODULE_MAP.md, and compliance reports generated.",
"TIER": "STANDARD",
"SEMANTICS": "semantic_analysis, parser, map_generator, compliance_checker, tier_validation, svelte_props, data_flow, module_map",
"LAYER": "DevOps/Tooling",
"INVARIANT": "All DEF anchors must have matching closing anchors; TIER determines validation strictness."
},
"relations": [
{
"type": "READS",
"target": "FileSystem"
},
{
"type": "PRODUCES",
"target": "semantics/semantic_map.json"
},
{
"type": "PRODUCES",
"target": ".ai/PROJECT_MAP.md"
},
{
"type": "PRODUCES",
"target": ".ai/MODULE_MAP.md"
},
{
"type": "PRODUCES",
"target": "semantics/reports/semantic_report_*.md"
}
],
"children": [
{
"name": "__init__",
"type": "Function",
"tier": "TRIVIAL",
"start_line": 27,
"end_line": 34,
"tags": {
"TIER": "TRIVIAL",
"PURPOSE": "Mock init for self-containment.",
"PRE": "name is a string.",
"POST": "Instance initialized."
},
"relations": [],
"children": [],
"compliance": {
"valid": true,
"issues": [],
"score": 1.0
}
},
{
"name": "__enter__",
"type": "Function",
"tier": "TRIVIAL",
"start_line": 36,
"end_line": 43,
"tags": {
"TIER": "TRIVIAL",
"PURPOSE": "Mock enter.",
"PRE": "Instance initialized.",
"POST": "Returns self."
},
"relations": [],
"children": [],
"compliance": {
"valid": true,
"issues": [],
"score": 1.0
}
},
{
"name": "__exit__",
"type": "Function",
"tier": "TRIVIAL",
"start_line": 45,
"end_line": 52,
"tags": {
"TIER": "TRIVIAL",
"PURPOSE": "Mock exit.",
"PRE": "Context entered.",
"POST": "Context exited."
},
"relations": [],
"children": [],
"compliance": {
"valid": true,
"issues": [],
"score": 1.0
}
},
{
"name": "to_dict",
"type": "Function",
"tier": "TRIVIAL",
"start_line": 138,
"end_line": 138,
"tags": {
"PURPOSE": "Auto-detected function (orphan)",
"TIER": "TRIVIAL"
},
"relations": [],
"children": [],
"compliance": {
"valid": true,
"issues": [],
"score": 1.0
}
}
],
"compliance": {
"valid": true,
"issues": [],
"score": 1.0
}
}

View File

@@ -1,9 +1,10 @@
# [DEF:generate_semantic_map:Module] # [DEF:generate_semantic_map:Module]
# # @PURPOSE: Scans the codebase to generate a Semantic Map, Module Map, and Compliance Report based on the System Standard.
# @TIER: CRITICAL # @PRE: Valid directory containing code to scan.
# @POST: Files map.json, .ai/PROJECT_MAP.md, .ai/MODULE_MAP.md, and compliance reports generated.
# @TIER: STANDARD
# @SEMANTICS: semantic_analysis, parser, map_generator, compliance_checker, tier_validation, svelte_props, data_flow, module_map # @SEMANTICS: semantic_analysis, parser, map_generator, compliance_checker, tier_validation, svelte_props, data_flow, module_map
# @PURPOSE: Scans the codebase to generate a Semantic Map, Module Map, and Compliance Report based on the System Standard. # @LAYER: DevOps/Tooling
# @LAYER: DevOps/Tooling
# @INVARIANT: All DEF anchors must have matching closing anchors; TIER determines validation strictness. # @INVARIANT: All DEF anchors must have matching closing anchors; TIER determines validation strictness.
# @RELATION: READS -> FileSystem # @RELATION: READS -> FileSystem
# @RELATION: PRODUCES -> semantics/semantic_map.json # @RELATION: PRODUCES -> semantics/semantic_map.json
@@ -262,7 +263,7 @@ class SemanticEntity:
# Check if it's a special case (logger.py or mock functions) # Check if it's a special case (logger.py or mock functions)
if "logger.py" not in self.file_path and "__" not in self.name: if "logger.py" not in self.file_path and "__" not in self.name:
severity = Severity.ERROR if tier == Tier.CRITICAL else Severity.WARNING severity = Severity.ERROR if tier == Tier.CRITICAL else Severity.WARNING
log_type = "belief_scope" if is_python else "console.log with [ID][STATE]" log_type = "belief_scope / molecular methods" if is_python else "console.log with [ID][STATE]"
self.compliance_issues.append(ComplianceIssue( self.compliance_issues.append(ComplianceIssue(
f"Missing Belief State Logging: Function should use {log_type} (required for {tier.value} tier)", f"Missing Belief State Logging: Function should use {log_type} (required for {tier.value} tier)",
severity, severity,
@@ -296,13 +297,17 @@ class SemanticEntity:
tier = self.get_tier() tier = self.get_tier()
score = 1.0 score = 1.0
# Dynamic penalties based on Tier
error_penalty = 0.5 if tier == Tier.CRITICAL else 0.3
warning_penalty = 0.15
# Count issues by severity # Count issues by severity
errors = len([i for i in self.compliance_issues if i.severity == Severity.ERROR]) errors = len([i for i in self.compliance_issues if i.severity == Severity.ERROR])
warnings = len([i for i in self.compliance_issues if i.severity == Severity.WARNING]) warnings = len([i for i in self.compliance_issues if i.severity == Severity.WARNING])
# Penalties # Penalties
score -= errors * 0.3 score -= errors * error_penalty
score -= warnings * 0.1 score -= warnings * warning_penalty
# Check mandatory tags # Check mandatory tags
required = TIER_MANDATORY_TAGS.get(tier, {}).get(self.type, []) required = TIER_MANDATORY_TAGS.get(tier, {}).get(self.type, [])
@@ -314,7 +319,8 @@ class SemanticEntity:
found_count += 1 found_count += 1
break break
if found_count < len(required): if found_count < len(required):
score -= 0.2 * (1 - (found_count / len(required))) missing_ratio = 1 - (found_count / len(required))
score -= 0.3 * missing_ratio
return max(0.0, score) return max(0.0, score)
# [/DEF:get_score:Function] # [/DEF:get_score:Function]
@@ -336,7 +342,8 @@ def get_patterns(lang: str) -> Dict[str, Pattern]:
"tag": re.compile(r"#\s*@(?P<tag>[A-Z_]+):\s*(?P<value>.*)"), "tag": re.compile(r"#\s*@(?P<tag>[A-Z_]+):\s*(?P<value>.*)"),
"relation": re.compile(r"#\s*@RELATION:\s*(?P<type>\w+)\s*->\s*(?P<target>.*)"), "relation": re.compile(r"#\s*@RELATION:\s*(?P<type>\w+)\s*->\s*(?P<target>.*)"),
"func_def": re.compile(r"^\s*(async\s+)?def\s+(?P<name>\w+)"), "func_def": re.compile(r"^\s*(async\s+)?def\s+(?P<name>\w+)"),
"belief_scope": re.compile(r"with\s+(\w+\.)?belief_scope\("), "belief_scope": re.compile(r"with\s+(\w+\.)?belief_scope\(|@believed\("),
"molecular_log": re.compile(r"logger\.(explore|reason|reflect)\("),
} }
else: else:
return { return {
@@ -348,7 +355,7 @@ def get_patterns(lang: str) -> Dict[str, Pattern]:
"jsdoc_tag": re.compile(r"\*\s*@(?P<tag>[a-zA-Z]+)\s+(?P<value>.*)"), "jsdoc_tag": re.compile(r"\*\s*@(?P<tag>[a-zA-Z]+)\s+(?P<value>.*)"),
"relation": re.compile(r"//\s*@RELATION:\s*(?P<type>\w+)\s*->\s*(?P<target>.*)"), "relation": re.compile(r"//\s*@RELATION:\s*(?P<type>\w+)\s*->\s*(?P<target>.*)"),
"func_def": re.compile(r"^\s*(export\s+)?(async\s+)?function\s+(?P<name>\w+)"), "func_def": re.compile(r"^\s*(export\s+)?(async\s+)?function\s+(?P<name>\w+)"),
"console_log": re.compile(r"console\.log\s*\(\s*['\"]\[[\w_]+\]\[[\w_]+\]"), "console_log": re.compile(r"console\.log\s*\(\s*['\"`]\[[\w_]+\]\[[A-Za-z0-9_:]+\]"),
# Svelte-specific patterns # Svelte-specific patterns
"export_let": re.compile(r"export\s+let\s+(?P<name>\w+)(?:\s*:\s*(?P<type>[\w\[\]|<>]+))?(?:\s*=\s*(?P<default>[^;]+))?"), "export_let": re.compile(r"export\s+let\s+(?P<name>\w+)(?:\s*:\s*(?P<type>[\w\[\]|<>]+))?(?:\s*=\s*(?P<default>[^;]+))?"),
"create_event_dispatcher": re.compile(r"createEventDispatcher\s*<\s*\{\s*(?P<events>[^}]+)\s*\}\s*\>"), "create_event_dispatcher": re.compile(r"createEventDispatcher\s*<\s*\{\s*(?P<events>[^}]+)\s*\}\s*\>"),
@@ -609,8 +616,10 @@ def parse_file(full_path: str, rel_path: str, lang: str) -> Tuple[List[SemanticE
current.tags[tag_name] = tag_value current.tags[tag_name] = tag_value
# Check for belief scope in implementation # Check for belief scope in implementation
if lang == "python" and "belief_scope" in patterns: if lang == "python":
if patterns["belief_scope"].search(line): if "belief_scope" in patterns and patterns["belief_scope"].search(line):
current.has_belief_scope = True
elif "molecular_log" in patterns and patterns["molecular_log"].search(line):
current.has_belief_scope = True current.has_belief_scope = True
# Check for console.log belief state in Svelte # Check for console.log belief state in Svelte
@@ -803,26 +812,39 @@ class SemanticMapGenerator:
with belief_scope("_process_file_results"): with belief_scope("_process_file_results"):
total_score = 0 total_score = 0
count = 0 count = 0
module_max_tier = Tier.TRIVIAL
# [DEF:validate_recursive:Function] # [DEF:validate_recursive:Function]
# @TIER: STANDARD # @TIER: STANDARD
# @PURPOSE: Recursively validates a list of entities. # @PURPOSE: Calculate score and determine module's max tier for weighted global score
# @PRE: ent_list is a list of SemanticEntity objects. # @PRE: Entities exist
# @POST: All entities and their children are validated. # @POST: Entities are validated
def validate_recursive(ent_list): def validate_recursive(ent_list):
with belief_scope("validate_recursive"): with belief_scope("validate_recursive"):
nonlocal total_score, count nonlocal total_score, count, module_max_tier
for e in ent_list: for e in ent_list:
e.validate() e.validate()
total_score += e.get_score() total_score += e.get_score()
count += 1 count += 1
# Determine dominant tier for file
e_tier = e.get_tier()
if e_tier == Tier.CRITICAL:
module_max_tier = Tier.CRITICAL
elif e_tier == Tier.STANDARD and module_max_tier != Tier.CRITICAL:
module_max_tier = Tier.STANDARD
validate_recursive(e.children) validate_recursive(e.children)
# [/DEF:validate_recursive:Function] # [/DEF:validate_recursive:Function]
validate_recursive(entities) validate_recursive(entities)
self.entities.extend(entities) self.entities.extend(entities)
self.file_scores[rel_path] = (total_score / count) if count > 0 else 0.0
# Store both the score and the dominating tier for weighted global calculation
file_score = (total_score / count) if count > 0 else 0.0
self.file_scores[rel_path] = {"score": file_score, "tier": module_max_tier}
# [/DEF:_process_file_results:Function] # [/DEF:_process_file_results:Function]
# [DEF:_generate_artifacts:Function] # [DEF:_generate_artifacts:Function]
@@ -860,7 +882,19 @@ class SemanticMapGenerator:
os.makedirs(REPORTS_DIR, exist_ok=True) os.makedirs(REPORTS_DIR, exist_ok=True)
total_files = len(self.file_scores) total_files = len(self.file_scores)
avg_score = sum(self.file_scores.values()) / total_files if total_files > 0 else 0
total_weighted_score = 0
total_weight = 0
for file_path, data in self.file_scores.items():
tier = data["tier"]
score = data["score"]
weight = 3 if tier == Tier.CRITICAL else (2 if tier == Tier.STANDARD else 1)
total_weighted_score += score * weight
total_weight += weight
avg_score = total_weighted_score / total_weight if total_weight > 0 else 0
# Count issues by severity # Count issues by severity
error_count = len([i for i in self.global_issues if i.severity == Severity.ERROR]) error_count = len([i for i in self.global_issues if i.severity == Severity.ERROR])
@@ -884,12 +918,19 @@ class SemanticMapGenerator:
f.write("| File | Score | Tier | Issues |\n") f.write("| File | Score | Tier | Issues |\n")
f.write("|------|-------|------|--------|\n") f.write("|------|-------|------|--------|\n")
sorted_files = sorted(self.file_scores.items(), key=lambda x: x[1]) # Sort logically: Critical first, then by score
sorted_files = sorted(self.file_scores.items(), key=lambda x: (
0 if x[1]["tier"] == Tier.CRITICAL else (1 if x[1]["tier"] == Tier.STANDARD else 2),
x[1]["score"]
))
for file_path, score in sorted_files: for file_path, data in sorted_files:
score = data["score"]
issues = [] issues = []
tier = "N/A" tier = "N/A"
self._collect_issues(self.entities, file_path, issues, tier) self._collect_issues(self.entities, file_path, issues, tier)
# Override Display Tier with the dominant tier we computed
tier = data["tier"].value
status_icon = "🟢" if score == 1.0 else "🟡" if score > 0.5 else "🔴" status_icon = "🟢" if score == 1.0 else "🟡" if score > 0.5 else "🔴"
issue_text = "<br>".join([f"{'🔴' if i.severity == Severity.ERROR else '🟡'} {i.message}" for i in issues[:3]]) issue_text = "<br>".join([f"{'🔴' if i.severity == Severity.ERROR else '🟡'} {i.message}" for i in issues[:3]])
@@ -1024,12 +1065,13 @@ class SemanticMapGenerator:
# @PRE: file_path is a valid relative path. # @PRE: file_path is a valid relative path.
# @POST: Returns a module path string. # @POST: Returns a module path string.
def _get_module_path(file_path: str) -> str: def _get_module_path(file_path: str) -> str:
# Convert file path to module-like path with belief_scope("_get_module_path"):
parts = file_path.replace(os.sep, '/').split('/') # Convert file path to module-like path
# Remove filename parts = file_path.replace(os.sep, '/').split('/')
if len(parts) > 1: # Remove filename
return '/'.join(parts[:-1]) if len(parts) > 1:
return 'root' return '/'.join(parts[:-1])
return 'root'
# [/DEF:_get_module_path:Function] # [/DEF:_get_module_path:Function]
# [DEF:_collect_all_entities:Function] # [DEF:_collect_all_entities:Function]
@@ -1038,9 +1080,10 @@ class SemanticMapGenerator:
# @PRE: entity list is valid. # @PRE: entity list is valid.
# @POST: Returns flat list of all entities with their hierarchy. # @POST: Returns flat list of all entities with their hierarchy.
def _collect_all_entities(entities: List[SemanticEntity], result: List[Tuple[str, SemanticEntity]]): def _collect_all_entities(entities: List[SemanticEntity], result: List[Tuple[str, SemanticEntity]]):
for e in entities: with belief_scope("_collect_all_entities"):
result.append((_get_module_path(e.file_path), e)) for e in entities:
_collect_all_entities(e.children, result) result.append((_get_module_path(e.file_path), e))
_collect_all_entities(e.children, result)
# [/DEF:_collect_all_entities:Function] # [/DEF:_collect_all_entities:Function]
# Collect all entities # Collect all entities

0
pers_module.json Normal file
View File

File diff suppressed because it is too large Load Diff

20
test_analyze.py Normal file
View File

@@ -0,0 +1,20 @@
import json
with open("semantics/semantic_map.json") as f:
data = json.load(f)
for m in data.get("modules", []):
if m.get("name") == "backend.src.core.task_manager.persistence":
def print_issues(node, depth=0):
issues = node.get("compliance", {}).get("issues", [])
if issues:
print(" "*depth, f"{node.get('type')} {node.get('name')} (line {node.get('start_line')}):")
for i in issues:
print(" "*(depth+1), "-", i.get("message"))
for c in node.get("children", []):
print_issues(c, depth+1)
for k in ["functions", "classes", "components"]:
for c in node.get(k, []):
print_issues(c, depth+1)
print_issues(m)

25
test_parse.py Normal file
View File

@@ -0,0 +1,25 @@
import re
patterns = {
"console_log": re.compile(r"console\.log\s*\(\s*['\"]\[[\w_]+\]\[[A-Za-z0-9_:]+\]"),
"js_anchor_start": re.compile(r"//\s*\[DEF:(?P<name>[\w\.]+):(?P<type>\w+)\]"),
"js_anchor_end": re.compile(r"//\s*\[/DEF:(?P<name>[\w\.]+)(?::\w+)?\]"),
"html_anchor_start": re.compile(r"<!--\s*\[DEF:(?P<name>[\w\.]+):(?P<type>\w+)\]\s*-->"),
"html_anchor_end": re.compile(r"<!--\s*\[/DEF:(?P<name>[\w\.]+)(?::\w+)?\]\s*-->"),
}
stack = []
with open("frontend/src/lib/components/assistant/AssistantChatPanel.svelte") as f:
for i, line in enumerate(f):
line_stripped = line.strip()
m_start = patterns["html_anchor_start"].search(line_stripped) or patterns["js_anchor_start"].search(line_stripped)
if m_start:
stack.append(m_start.group("name"))
m_end = patterns["html_anchor_end"].search(line_stripped) or patterns["js_anchor_end"].search(line_stripped)
if m_end:
stack.pop()
if patterns["console_log"].search(line):
print(f"Matched console.log on line {i+1} while stack is {stack}")

10
test_parse2.py Normal file
View File

@@ -0,0 +1,10 @@
import re
patterns = {
"console_log": re.compile(r"console\.log\s*\(\s*['\"]\[[\w_]+\]\[[A-Za-z0-9_:]+\]"),
}
with open("frontend/src/lib/components/assistant/AssistantChatPanel.svelte") as f:
for i, line in enumerate(f):
if "console.log" in line:
m = patterns["console_log"].search(line)
print(f"Line {i+1}: {line.strip()} -> Match: {bool(m)}")

227
test_parser.py Normal file
View File

@@ -0,0 +1,227 @@
# [DEF:backend.src.services.reports.report_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: reports, service, aggregation, filtering, pagination, detail
# @PURPOSE: Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.core.task_manager.manager.TaskManager
# @RELATION: DEPENDS_ON -> backend.src.models.report
# @RELATION: DEPENDS_ON -> backend.src.services.reports.normalizer
# @INVARIANT: List responses are deterministic and include applied filter echo metadata.
# [SECTION: IMPORTS]
from datetime import datetime, timezone
from typing import List, Optional
from ...core.logger import belief_scope
from ...core.task_manager import TaskManager
from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskReport, TaskType
from .normalizer import normalize_task_report
# [/SECTION]
# [DEF:ReportsService:Class]
# @PURPOSE: Service layer for list/detail report retrieval and normalization.
# @TIER: CRITICAL
# @PRE: TaskManager dependency is initialized.
# @POST: Provides deterministic list/detail report responses.
# @INVARIANT: Service methods are read-only over task history source.
class ReportsService:
# [DEF:__init__:Function]
# @TIER: CRITICAL
# @PURPOSE: Initialize service with TaskManager dependency.
# @PRE: task_manager is a live TaskManager instance.
# @POST: self.task_manager is assigned and ready for read operations.
# @INVARIANT: Constructor performs no task mutations.
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
def __init__(self, task_manager: TaskManager):
with belief_scope("__init__"):
self.task_manager = task_manager
# [/DEF:__init__:Function]
# [DEF:_load_normalized_reports:Function]
# @PURPOSE: Build normalized reports from all available tasks.
# @PRE: Task manager returns iterable task history records.
# @POST: Returns normalized report list preserving source cardinality.
# @INVARIANT: Every returned item is a TaskReport.
# @RETURN: List[TaskReport] - Reports sorted later by list logic.
def _load_normalized_reports(self) -> List[TaskReport]:
with belief_scope("_load_normalized_reports"):
tasks = self.task_manager.get_all_tasks()
reports = [normalize_task_report(task) for task in tasks]
return reports
# [/DEF:_load_normalized_reports:Function]
# [DEF:_to_utc_datetime:Function]
# @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
# @PRE: value is either datetime or None.
# @POST: Returns UTC-aware datetime or None.
# @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
# @PARAM: value (Optional[datetime]) - Source datetime value.
# @RETURN: Optional[datetime] - UTC-aware datetime or None.
def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:
with belief_scope("_to_utc_datetime"):
if value is None:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
# [/DEF:_to_utc_datetime:Function]
# [DEF:_datetime_sort_key:Function]
# @PURPOSE: Produce stable numeric sort key for report timestamps.
# @PRE: report contains updated_at datetime.
# @POST: Returns float timestamp suitable for deterministic sorting.
# @INVARIANT: Mixed naive/aware datetimes never raise TypeError.
# @PARAM: report (TaskReport) - Report item.
# @RETURN: float - UTC timestamp key.
def _datetime_sort_key(self, report: TaskReport) -> float:
with belief_scope("_datetime_sort_key"):
updated = self._to_utc_datetime(report.updated_at)
if updated is None:
return 0.0
return updated.timestamp()
# [/DEF:_datetime_sort_key:Function]
# [DEF:_matches_query:Function]
# @PURPOSE: Apply query filtering to a report.
# @PRE: report and query are normalized schema instances.
# @POST: Returns True iff report satisfies all active query filters.
# @INVARIANT: Filter evaluation is side-effect free.
# @PARAM: report (TaskReport) - Candidate report.
# @PARAM: query (ReportQuery) - Applied query.
# @RETURN: bool - True if report matches all filters.
def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
with belief_scope("_matches_query"):
if query.task_types and report.task_type not in query.task_types:
return False
if query.statuses and report.status not in query.statuses:
return False
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
if needle not in haystack:
return False
return True
# [/DEF:_matches_query:Function]
# [DEF:_sort_reports:Function]
# @PURPOSE: Sort reports deterministically according to query settings.
# @PRE: reports contains only TaskReport items.
# @POST: Returns reports ordered by selected sort field and order.
# @INVARIANT: Sorting criteria are deterministic for equal input.
# @PARAM: reports (List[TaskReport]) - Filtered reports.
# @PARAM: query (ReportQuery) - Sort config.
# @RETURN: List[TaskReport] - Sorted reports.
def _sort_reports(self, reports: List[TaskReport], query: ReportQuery) -> List[TaskReport]:
with belief_scope("_sort_reports"):
reverse = query.sort_order == "desc"
if query.sort_by == "status":
reports.sort(key=lambda item: item.status.value, reverse=reverse)
elif query.sort_by == "task_type":
reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
else:
reports.sort(key=self._datetime_sort_key, reverse=reverse)
return reports
# [/DEF:_sort_reports:Function]
# [DEF:list_reports:Function]
# @PURPOSE: Return filtered, sorted, paginated report collection.
# @PRE: query has passed schema validation.
# @POST: Returns {items,total,page,page_size,has_next,applied_filters}.
# @PARAM: query (ReportQuery) - List filters and pagination.
# @RETURN: ReportCollection - Paginated unified reports payload.
def list_reports(self, query: ReportQuery) -> ReportCollection:
with belief_scope("list_reports"):
reports = self._load_normalized_reports()
filtered = [report for report in reports if self._matches_query(report, query)]
sorted_reports = self._sort_reports(filtered, query)
total = len(sorted_reports)
start = (query.page - 1) * query.page_size
end = start + query.page_size
items = sorted_reports[start:end]
has_next = end < total
return ReportCollection(
items=items,
total=total,
page=query.page,
page_size=query.page_size,
has_next=has_next,
applied_filters=query,
)
# [/DEF:list_reports:Function]
# [DEF:get_report_detail:Function]
# @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.
# @PRE: report_id exists in normalized report set.
# @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.
# @PARAM: report_id (str) - Stable report identifier.
# @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.
def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]:
with belief_scope("get_report_detail"):
reports = self._load_normalized_reports()
target = next((report for report in reports if report.report_id == report_id), None)
if not target:
return None
timeline = []
if target.started_at:
timeline.append({"event": "started", "at": target.started_at.isoformat()})
timeline.append({"event": "updated", "at": target.updated_at.isoformat()})
diagnostics = target.details or {}
if not diagnostics:
diagnostics = {"note": "Not provided"}
if target.error_context:
diagnostics["error_context"] = target.error_context.model_dump()
next_actions = []
if target.error_context and target.error_context.next_actions:
next_actions = target.error_context.next_actions
elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
next_actions = ["Review diagnostics", "Retry task if applicable"]
return ReportDetailView(
report=target,
timeline=timeline,
diagnostics=diagnostics,
next_actions=next_actions,
)
# [/DEF:get_report_detail:Function]
# [/DEF:ReportsService:Class]
import sys
from generate_semantic_map import parse_file
file_path = "backend/src/core/task_manager/task_logger.py"
entities, issues = parse_file(file_path, file_path, "python")
for e in entities:
e.validate()
def print_entity(ent, indent=0):
print(" " * indent + f"{ent.type} {ent.name} Tags: {list(ent.tags.keys())} Belief: {ent.has_belief_scope}")
for i in ent.compliance_issues:
print(" " * (indent + 1) + f"ISSUE: {i.message}")
for c in ent.children:
print_entity(c, indent + 1)
for e in entities:
print_entity(e)
for i in issues:
print(f"GLOBAL ISSUE: {i.message} at line {i.line_number}")
# [/DEF:backend.src.services.reports.report_service:Module]

13
test_regex.py Normal file
View File

@@ -0,0 +1,13 @@
import re
patterns = {
"console_log": re.compile(r"console\.log\s*\(\s*['\"]\[[\w_]+\]\[[A-Za-z0-9_:]+\]"),
}
with open("frontend/src/lib/components/assistant/AssistantChatPanel.svelte") as f:
for i, line in enumerate(f):
if "console.log" in line:
if patterns["console_log"].search(line):
print(f"Match: {line.strip()}")
else:
print(f"No match: {line.strip()}")