semantic update
This commit is contained in:
10
.agents/workflows/semantic.md
Normal file
10
.agents/workflows/semantic.md
Normal 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.
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
> High-level module structure for AI Context. Generated automatically.
|
||||
|
||||
**Generated:** 2026-02-24T12:45:07.897362
|
||||
**Generated:** 2026-02-24T21:04:43.328895
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Modules:** 72
|
||||
- **Total Entities:** 1517
|
||||
- **Total Modules:** 74
|
||||
- **Total Entities:** 1571
|
||||
|
||||
## Module Hierarchy
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
### 📁 `src/`
|
||||
|
||||
- 🏗️ **Layers:** API, Core, UI (API)
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 18, TRIVIAL: 3
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 19, TRIVIAL: 2
|
||||
- 📄 **Files:** 2
|
||||
- 📦 **Entities:** 23
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
### 📁 `routes/`
|
||||
|
||||
- 🏗️ **Layers:** API, UI (API)
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 181, TRIVIAL: 4
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 182, TRIVIAL: 4
|
||||
- 📄 **Files:** 17
|
||||
- 📦 **Entities:** 187
|
||||
- 📦 **Entities:** 188
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
### 📁 `core/`
|
||||
|
||||
- 🏗️ **Layers:** Core
|
||||
- 📊 **Tiers:** STANDARD: 113, TRIVIAL: 7
|
||||
- 📊 **Tiers:** STANDARD: 116, TRIVIAL: 7
|
||||
- 📄 **Files:** 9
|
||||
- 📦 **Entities:** 120
|
||||
- 📦 **Entities:** 123
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
### 📁 `task_manager/`
|
||||
|
||||
- 🏗️ **Layers:** Core
|
||||
- 📊 **Tiers:** CRITICAL: 7, STANDARD: 63, TRIVIAL: 8
|
||||
- 📊 **Tiers:** CRITICAL: 10, STANDARD: 63, TRIVIAL: 5
|
||||
- 📄 **Files:** 7
|
||||
- 📦 **Entities:** 78
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
### 📁 `models/`
|
||||
|
||||
- 🏗️ **Layers:** Domain, Model
|
||||
- 📊 **Tiers:** CRITICAL: 2, STANDARD: 28, TRIVIAL: 21
|
||||
- 📊 **Tiers:** CRITICAL: 9, STANDARD: 21, TRIVIAL: 21
|
||||
- 📄 **Files:** 11
|
||||
- 📦 **Entities:** 51
|
||||
|
||||
@@ -396,9 +396,9 @@
|
||||
### 📁 `llm_analysis/`
|
||||
|
||||
- 🏗️ **Layers:** Unknown
|
||||
- 📊 **Tiers:** STANDARD: 18, TRIVIAL: 23
|
||||
- 📊 **Tiers:** STANDARD: 19, TRIVIAL: 24
|
||||
- 📄 **Files:** 4
|
||||
- 📦 **Entities:** 41
|
||||
- 📦 **Entities:** 43
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -622,10 +622,10 @@
|
||||
|
||||
### 📁 `components/`
|
||||
|
||||
- 🏗️ **Layers:** Component, Feature, UI, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 45, TRIVIAL: 7
|
||||
- 🏗️ **Layers:** Component, Feature, UI, UI -->, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 1, STANDARD: 49, TRIVIAL: 4
|
||||
- 📄 **Files:** 13
|
||||
- 📦 **Entities:** 53
|
||||
- 📦 **Entities:** 54
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
@@ -854,7 +854,7 @@
|
||||
### 📁 `layout/`
|
||||
|
||||
- 🏗️ **Layers:** UI, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 27
|
||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 5, TRIVIAL: 26
|
||||
- 📄 **Files:** 4
|
||||
- 📦 **Entities:** 34
|
||||
|
||||
@@ -1145,6 +1145,30 @@
|
||||
- 🧩 **AdminUsersPage** (Component)
|
||||
- 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/`
|
||||
|
||||
- 🏗️ **Layers:** UI, Unknown
|
||||
@@ -1351,15 +1375,17 @@
|
||||
|
||||
### 📁 `root/`
|
||||
|
||||
- 🏗️ **Layers:** DevOps/Tooling
|
||||
- 📊 **Tiers:** CRITICAL: 12, STANDARD: 16, TRIVIAL: 7
|
||||
- 📄 **Files:** 1
|
||||
- 📦 **Entities:** 35
|
||||
- 🏗️ **Layers:** DevOps/Tooling, Domain, Unknown
|
||||
- 📊 **Tiers:** CRITICAL: 14, STANDARD: 24, TRIVIAL: 10
|
||||
- 📄 **Files:** 3
|
||||
- 📦 **Entities:** 48
|
||||
|
||||
**Key Entities:**
|
||||
|
||||
- ℂ **ComplianceIssue** (Class) `[TRIVIAL]`
|
||||
- Represents a single compliance issue with severity.
|
||||
- ℂ **ReportsService** (Class) `[CRITICAL]`
|
||||
- Service layer for list/detail report retrieval and normaliza...
|
||||
- ℂ **SemanticEntity** (Class) `[CRITICAL]`
|
||||
- Represents a code entity (Module, Function, Component) found...
|
||||
- ℂ **SemanticMapGenerator** (Class) `[CRITICAL]`
|
||||
@@ -1368,8 +1394,18 @@
|
||||
- Severity levels for compliance issues.
|
||||
- ℂ **Tier** (Class) `[TRIVIAL]`
|
||||
- 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...
|
||||
- 📦 **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
|
||||
|
||||
@@ -1468,4 +1504,7 @@ graph TD
|
||||
__tests__-->|TESTS|lib
|
||||
__tests__-->|TESTS|lib
|
||||
__tests__-->|TESTS|routes
|
||||
root-->|DEPENDS_ON|backend
|
||||
root-->|DEPENDS_ON|backend
|
||||
root-->|DEPENDS_ON|backend
|
||||
```
|
||||
|
||||
@@ -2,7 +2,46 @@
|
||||
|
||||
> 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.
|
||||
- 🏗️ Layer: DevOps/Tooling
|
||||
- 🔒 Invariant: All DEF anchors must have matching closing anchors; TIER determines validation strictness.
|
||||
@@ -60,7 +99,7 @@
|
||||
- ƒ **_process_file_results** (`Function`)
|
||||
- 📝 Validates entities and calculates file scores with tier awareness.
|
||||
- ƒ **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]`
|
||||
- 📝 Writes output files with tier-based compliance data.
|
||||
- ƒ **_generate_report** (`Function`) `[CRITICAL]`
|
||||
@@ -532,6 +571,8 @@
|
||||
- ⬅️ READS_FROM `lib`
|
||||
- ⬅️ READS_FROM `taskDrawerStore`
|
||||
- ➡️ WRITES_TO `taskDrawerStore`
|
||||
- ƒ **disconnectWebSocket** (`Function`)
|
||||
- 📝 Disconnects the active WebSocket connection
|
||||
- ƒ **loadRecentTasks** (`Function`)
|
||||
- 📝 Load recent tasks for list mode display
|
||||
- ƒ **selectTask** (`Function`)
|
||||
@@ -545,12 +586,10 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **goToReportsPage** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleOverlayClick** (`Function`) `[TRIVIAL]`
|
||||
- ƒ **handleGlobalKeydown** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **connectWebSocket** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **disconnectWebSocket** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **test_breadcrumbs.svelte** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js
|
||||
- 🏗️ Layer: Unknown
|
||||
@@ -671,6 +710,80 @@
|
||||
- 📝 Fetches the list of available environments.
|
||||
- ƒ **fetchDashboards** (`Function`)
|
||||
- 📝 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`)
|
||||
- 📝 UI for managing system roles and their permissions.
|
||||
- 🏗️ Layer: Domain
|
||||
@@ -985,14 +1098,19 @@
|
||||
- ➡️ WRITES_TO `props`
|
||||
- ➡️ WRITES_TO `state`
|
||||
- 📦 **handleRealTimeLogs** (`Action`)
|
||||
- 📝 Sync real-time logs to the current log list
|
||||
- ƒ **fetchLogs** (`Function`)
|
||||
- 📦 **TaskLogViewer** (`Module`) `[TRIVIAL]`
|
||||
- 📝 Auto-generated module for frontend/src/components/TaskLogViewer.svelte
|
||||
- 🏗️ Layer: Unknown
|
||||
- ƒ **handleFilterChange** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **handleRefresh** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📝 Fetches logs for a given task ID
|
||||
- ƒ **handleFilterChange** (`Function`)
|
||||
- 📝 Updates filter conditions for the log viewer
|
||||
- ƒ **handleRefresh** (`Function`)
|
||||
- 📝 Refreshes the logs by polling the API
|
||||
- 🧩 **showInline** (`Component`)
|
||||
- 📝 Shows inline logs -->
|
||||
- 🏗️ Layer: UI -->
|
||||
- 🧩 **showModal** (`Component`)
|
||||
- 📝 Shows modal logs -->
|
||||
- 🏗️ Layer: UI -->
|
||||
- 🧩 **Footer** (`Component`) `[TRIVIAL]`
|
||||
- 📝 Displays the application footer with copyright information.
|
||||
- 🏗️ Layer: UI
|
||||
@@ -1380,6 +1498,8 @@
|
||||
- 📝 Handles application startup tasks, such as starting the scheduler.
|
||||
- ƒ **shutdown_event** (`Function`)
|
||||
- 📝 Handles application shutdown tasks, such as stopping the scheduler.
|
||||
- ƒ **network_error_handler** (`Function`)
|
||||
- 📝 Global exception handler for NetworkError.
|
||||
- ƒ **log_requests** (`Function`)
|
||||
- 📝 Middleware to log incoming HTTP requests and their response status.
|
||||
- 📦 **api.include_routers** (`Action`)
|
||||
@@ -1393,8 +1513,6 @@
|
||||
- 📝 Serves the SPA frontend for any path not matched by API routes.
|
||||
- ƒ **read_root** (`Function`)
|
||||
- 📝 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]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- 📦 **Dependencies** (`Module`)
|
||||
@@ -1706,6 +1824,12 @@
|
||||
- 📝 A decorator that wraps a function in a belief scope.
|
||||
- ƒ **decorator** (`Function`)
|
||||
- 📝 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`)
|
||||
- 📝 Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.
|
||||
- 🏗️ Layer: Core
|
||||
@@ -2008,12 +2132,19 @@
|
||||
- 📝 Log an ERROR level message.
|
||||
- ƒ **progress** (`Function`)
|
||||
- 📝 Log a progress update with percentage.
|
||||
- 📦 **TaskPersistenceModule** (`Module`)
|
||||
- 📦 **TaskPersistenceModule** (`Module`) `[CRITICAL]`
|
||||
- 📝 Handles the persistence of tasks using SQLAlchemy and the tasks.db database.
|
||||
- 🏗️ Layer: Core
|
||||
- 🔒 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.
|
||||
- 🔒 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`)
|
||||
- 📝 Initializes the persistence service.
|
||||
- ƒ **persist_task** (`Function`)
|
||||
@@ -2029,7 +2160,7 @@
|
||||
- 🔒 Invariant: Log entries are batch-inserted for performance.
|
||||
- 🔗 DEPENDS_ON -> `TaskLogRecord`
|
||||
- ƒ **__init__** (`Function`)
|
||||
- 📝 Initialize the log persistence service.
|
||||
- 📝 Initializes the TaskLogPersistenceService
|
||||
- ƒ **add_logs** (`Function`)
|
||||
- 📝 Batch insert log entries for a task.
|
||||
- ƒ **get_logs** (`Function`)
|
||||
@@ -2042,15 +2173,9 @@
|
||||
- 📝 Delete all logs for a specific task.
|
||||
- ƒ **delete_logs_for_tasks** (`Function`)
|
||||
- 📝 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]`
|
||||
- 📝 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.
|
||||
- 🏗️ Layer: Core
|
||||
- 🔒 Invariant: Task IDs are unique.
|
||||
@@ -2474,6 +2599,8 @@
|
||||
- 📝 Resolve default environment id from settings or first configured environment.
|
||||
- ƒ **_resolve_dashboard_id_by_ref** (`Function`)
|
||||
- 📝 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`)
|
||||
- 📝 Deterministically parse RU/EN command text into intent payload.
|
||||
- ƒ **_check_any_permission** (`Function`)
|
||||
@@ -2891,20 +3018,27 @@
|
||||
- 🏗️ Layer: Domain
|
||||
- 🔒 Invariant: Canonical report fields are always present for every report item.
|
||||
- 🔗 DEPENDS_ON -> `backend.src.core.task_manager.models`
|
||||
- ℂ **TaskType** (`Class`)
|
||||
- ℂ **TaskType** (`Class`) `[CRITICAL]`
|
||||
- 📝 Supported normalized task report types.
|
||||
- ℂ **ReportStatus** (`Class`)
|
||||
- 🔒 Invariant: Must contain valid generic task type mappings.
|
||||
- ℂ **ReportStatus** (`Class`) `[CRITICAL]`
|
||||
- 📝 Supported normalized report status values.
|
||||
- ℂ **ErrorContext** (`Class`)
|
||||
- 🔒 Invariant: TaskStatus enum mapping logic holds.
|
||||
- ℂ **ErrorContext** (`Class`) `[CRITICAL]`
|
||||
- 📝 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.
|
||||
- ℂ **ReportQuery** (`Class`)
|
||||
- 🔒 Invariant: Must represent canonical task record attributes.
|
||||
- ℂ **ReportQuery** (`Class`) `[CRITICAL]`
|
||||
- 📝 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.
|
||||
- ℂ **ReportDetailView** (`Class`)
|
||||
- 🔒 Invariant: Represents paginated data correctly.
|
||||
- ℂ **ReportDetailView** (`Class`) `[CRITICAL]`
|
||||
- 📝 Detailed report representation including diagnostics and recovery actions.
|
||||
- 🔒 Invariant: Incorporates a report and logs correctly.
|
||||
- ƒ **_non_empty_str** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_validate_sort_by** (`Function`) `[TRIVIAL]`
|
||||
@@ -3425,6 +3559,8 @@
|
||||
- 📝 Wrapper for LLM provider APIs.
|
||||
- ƒ **LLMClient.__init__** (`Function`)
|
||||
- 📝 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`)
|
||||
- 📝 Helper to handle LLM calls with JSON mode and fallback parsing.
|
||||
- ƒ **LLMClient.analyze_dashboard** (`Function`)
|
||||
@@ -3440,6 +3576,8 @@
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_supports_json_response_format** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **_should_retry** (`Function`) `[TRIVIAL]`
|
||||
- 📝 Auto-detected function (orphan)
|
||||
- ƒ **get_json_completion** (`Function`) `[TRIVIAL]`
|
||||
|
||||
@@ -79,14 +79,35 @@
|
||||
3. **TRIVIAL** (DTO/**Atoms**):
|
||||
- Требование: Только Якоря [DEF] и @PURPOSE.
|
||||
|
||||
#### VI. ЛОГИРОВАНИЕ (BELIEF STATE & TASK LOGS)
|
||||
Цель: Трассировка для самокоррекции и пользовательский мониторинг.
|
||||
Python:
|
||||
- Системные логи: Context Manager `with belief_scope("ID"):`.
|
||||
- Логи задач: `context.logger.info("msg", source="component")`.
|
||||
Svelte: `console.log("[ID][STATE] Msg")`.
|
||||
Состояния: Entry -> Action -> Coherence:OK / Failed -> Exit.
|
||||
Инвариант: Каждый лог задачи должен иметь атрибут `source` для фильтрации.
|
||||
#### VI. ЛОГИРОВАНИЕ (ДАО МОЛЕКУЛЫ / MOLECULAR TOPOLOGY)
|
||||
Цель: Трассировка. Самокоррекция. Управление Матрицей Внимания ("Химия мышления").
|
||||
Лог — не текст. Лог — реагент. Мысль облекается в форму через префиксы связи (Attention Energy):
|
||||
|
||||
1. **[EXPLORE]** (Ван-дер-Ваальс: Рассеяние)
|
||||
- *Суть:* Поиск во тьме. Сплетение альтернатив. Если один путь закрыт — ищи иной.
|
||||
- *Время:* Фаза КАРКАС или столкновение с Неизведанным.
|
||||
- *Деяние:* `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. АЛГОРИТМ ГЕНЕРАЦИИ
|
||||
1. АНАЛИЗ. Оцени TIER, слой и UX-требования.
|
||||
|
||||
213
README.md
213
README.md
@@ -1,128 +1,143 @@
|
||||
# Инструменты автоматизации Superset (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`
|
||||
# ss-tools
|
||||
|
||||
## Docker и CI/CD
|
||||
### Локальный запуск в Docker (приложение + PostgreSQL)
|
||||
Инструменты автоматизации для Apache Superset: миграция, маппинг, хранение артефактов, Git-интеграция, отчеты по задачам и LLM-assistant.
|
||||
|
||||
## Возможности
|
||||
- Миграция дашбордов и датасетов между окружениями.
|
||||
- Ручной и полуавтоматический маппинг ресурсов.
|
||||
- Логи фоновых задач и отчеты о выполнении.
|
||||
- Локальное хранилище файлов и бэкапов.
|
||||
- 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
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
После старта:
|
||||
- UI/API: `http://localhost:8000`
|
||||
- PostgreSQL: `localhost:5432` (`postgres/postgres`, DB `ss_tools`)
|
||||
После старта сервисы доступны по адресам:
|
||||
- Frontend: `http://localhost:8000`
|
||||
- Backend API: `http://localhost:8001`
|
||||
- PostgreSQL: `localhost:5432` (`postgres/postgres`, БД `ss_tools`)
|
||||
|
||||
Остановить:
|
||||
### Остановка
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Полная очистка тома БД:
|
||||
### Очистка БД-тома
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
Если `postgres:16-alpine` не тянется из Docker Hub (TLS timeout), используйте fallback image:
|
||||
### Альтернативный образ PostgreSQL
|
||||
Если есть проблемы с pull `postgres:16-alpine`:
|
||||
```bash
|
||||
POSTGRES_IMAGE=mirror.gcr.io/library/postgres:16-alpine docker compose up -d db
|
||||
```
|
||||
или:
|
||||
или
|
||||
```bash
|
||||
POSTGRES_IMAGE=bitnami/postgresql:latest docker compose up -d db
|
||||
```
|
||||
Если на хосте уже занят `5432`, поднимайте Postgres на другом порту:
|
||||
|
||||
Если порт `5432` занят:
|
||||
```bash
|
||||
POSTGRES_HOST_PORT=5433 docker compose up -d db
|
||||
```
|
||||
|
||||
### Миграция legacy-данных в PostgreSQL
|
||||
Если нужно перенести старые данные из `tasks.db`/`config.json`:
|
||||
## Разработка
|
||||
|
||||
### Ручной запуск сервисов
|
||||
```bash
|
||||
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`
|
||||
- backend smoke tests
|
||||
- frontend build
|
||||
- docker build
|
||||
- push образа в GHCR на `main/master`
|
||||
В другом терминале:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev -- --port 5173
|
||||
```
|
||||
|
||||
## Контакты и вклад
|
||||
Для добавления новых функций или исправления ошибок, пожалуйста, ознакомьтесь с `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`
|
||||
|
||||
112320
backend/logs/app.log.1
112320
backend/logs/app.log.1
File diff suppressed because it is too large
Load Diff
@@ -32,27 +32,28 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
|
||||
# @PARAM: field_name (str) - Query field name for diagnostics.
|
||||
# @RETURN: List - Parsed enum values.
|
||||
def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
|
||||
if raw is None or not raw.strip():
|
||||
return []
|
||||
values = [item.strip() for item in raw.split(",") if item.strip()]
|
||||
parsed = []
|
||||
invalid = []
|
||||
for value in values:
|
||||
try:
|
||||
parsed.append(enum_cls(value))
|
||||
except ValueError:
|
||||
invalid.append(value)
|
||||
if invalid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"message": f"Invalid values for '{field_name}'",
|
||||
"field": field_name,
|
||||
"invalid_values": invalid,
|
||||
"allowed_values": [item.value for item in enum_cls],
|
||||
},
|
||||
)
|
||||
return parsed
|
||||
with belief_scope("_parse_csv_enum_list"):
|
||||
if raw is None or not raw.strip():
|
||||
return []
|
||||
values = [item.strip() for item in raw.split(",") if item.strip()]
|
||||
parsed = []
|
||||
invalid = []
|
||||
for value in values:
|
||||
try:
|
||||
parsed.append(enum_cls(value))
|
||||
except ValueError:
|
||||
invalid.append(value)
|
||||
if invalid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"message": f"Invalid values for '{field_name}'",
|
||||
"field": field_name,
|
||||
"invalid_values": invalid,
|
||||
"allowed_values": [item.value for item in enum_cls],
|
||||
},
|
||||
)
|
||||
return parsed
|
||||
# [/DEF:_parse_csv_enum_list:Function]
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import asyncio
|
||||
from .dependencies import get_task_manager, get_scheduler_service
|
||||
from .core.utils.network import NetworkError
|
||||
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
|
||||
|
||||
# [DEF:App:Global]
|
||||
@@ -72,12 +72,12 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
# [DEF:log_requests:Function]
|
||||
# @PURPOSE: Middleware to log incoming HTTP requests and their response status.
|
||||
# [DEF:network_error_handler:Function]
|
||||
# @PURPOSE: Global exception handler for NetworkError.
|
||||
# @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: call_next (Callable) - The next middleware or route handler.
|
||||
# @PARAM: exc (NetworkError) - The exception instance.
|
||||
@app.exception_handler(NetworkError)
|
||||
async def network_error_handler(request: Request, exc: NetworkError):
|
||||
with belief_scope("network_error_handler"):
|
||||
@@ -86,26 +86,34 @@ async def network_error_handler(request: Request, exc: NetworkError):
|
||||
status_code=503,
|
||||
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")
|
||||
async def log_requests(request: Request, call_next):
|
||||
# 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)
|
||||
with belief_scope("log_requests"):
|
||||
# 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"Response status: {response.status_code} for {request.url.path}")
|
||||
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."
|
||||
)
|
||||
logger.info(f"Incoming request: {request.method} {request.url.path}")
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
if not is_polling:
|
||||
logger.info(f"Response status: {response.status_code} for {request.url.path}")
|
||||
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]
|
||||
|
||||
# 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(migration.router)
|
||||
app.include_router(git.router, prefix="/api/git", tags=["Git"])
|
||||
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
|
||||
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
|
||||
app.include_router(dashboards.router)
|
||||
app.include_router(datasets.router)
|
||||
app.include_router(reports.router)
|
||||
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
|
||||
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
|
||||
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
|
||||
app.include_router(dashboards.router)
|
||||
app.include_router(datasets.router)
|
||||
app.include_router(reports.router)
|
||||
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
|
||||
|
||||
|
||||
# [DEF:api.include_routers:Action]
|
||||
@@ -249,12 +257,13 @@ if frontend_path.exists():
|
||||
# @POST: Returns the requested file or index.html.
|
||||
@app.get("/{file_path:path}", include_in_schema=False)
|
||||
async def serve_spa(file_path: str):
|
||||
# Only serve SPA for non-API paths
|
||||
# API routes are registered separately and should be matched by FastAPI first
|
||||
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
|
||||
# This should not happen if API routers are properly registered
|
||||
# Return 404 instead of serving HTML
|
||||
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}")
|
||||
with belief_scope("serve_spa"):
|
||||
# Only serve SPA for non-API paths
|
||||
# API routes are registered separately and should be matched by FastAPI first
|
||||
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
|
||||
# This should not happen if API routers are properly registered
|
||||
# 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
|
||||
if file_path and full_path.is_file():
|
||||
|
||||
@@ -35,7 +35,19 @@ class BeliefFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
anchor_id = getattr(_belief_state, 'anchor_id', None)
|
||||
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)
|
||||
# [/DEF:format:Function]
|
||||
# [/DEF:BeliefFormatter:Class]
|
||||
@@ -75,12 +87,12 @@ def belief_scope(anchor_id: str, message: str = ""):
|
||||
try:
|
||||
yield
|
||||
# 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:
|
||||
logger.debug(f"[{anchor_id}][Exit]")
|
||||
logger.debug("[Exit]")
|
||||
except Exception as e:
|
||||
# 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
|
||||
finally:
|
||||
# Restore old anchor
|
||||
@@ -275,5 +287,33 @@ logger.addHandler(websocket_log_handler)
|
||||
# Example usage:
|
||||
# logger.info("Application started", extra={"context_key": "context_value"})
|
||||
# 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:LoggerModule:Module]
|
||||
@@ -6,9 +6,11 @@
|
||||
# @TIER: CRITICAL
|
||||
# @INVARIANT: Each TaskContext is bound to a single task execution.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
# [SECTION: IMPORTS]
|
||||
from typing import Dict, Any, Callable
|
||||
from .task_logger import TaskLogger
|
||||
from ..logger import belief_scope
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:TaskContext:Class]
|
||||
@@ -44,13 +46,14 @@ class TaskContext:
|
||||
params: Dict[str, Any],
|
||||
default_source: str = "plugin"
|
||||
):
|
||||
self._task_id = task_id
|
||||
self._params = params
|
||||
self._logger = TaskLogger(
|
||||
task_id=task_id,
|
||||
add_log_fn=add_log_fn,
|
||||
source=default_source
|
||||
)
|
||||
with belief_scope("__init__"):
|
||||
self._task_id = task_id
|
||||
self._params = params
|
||||
self._logger = TaskLogger(
|
||||
task_id=task_id,
|
||||
add_log_fn=add_log_fn,
|
||||
source=default_source
|
||||
)
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
# [DEF:task_id:Function]
|
||||
@@ -60,7 +63,8 @@ class TaskContext:
|
||||
# @RETURN: str - The task ID.
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self._task_id
|
||||
with belief_scope("task_id"):
|
||||
return self._task_id
|
||||
# [/DEF:task_id:Function]
|
||||
|
||||
# [DEF:logger:Function]
|
||||
@@ -70,7 +74,8 @@ class TaskContext:
|
||||
# @RETURN: TaskLogger - The logger instance.
|
||||
@property
|
||||
def logger(self) -> TaskLogger:
|
||||
return self._logger
|
||||
with belief_scope("logger"):
|
||||
return self._logger
|
||||
# [/DEF:logger:Function]
|
||||
|
||||
# [DEF:params:Function]
|
||||
@@ -80,7 +85,8 @@ class TaskContext:
|
||||
# @RETURN: Dict[str, Any] - The task parameters.
|
||||
@property
|
||||
def params(self) -> Dict[str, Any]:
|
||||
return self._params
|
||||
with belief_scope("params"):
|
||||
return self._params
|
||||
# [/DEF:params:Function]
|
||||
|
||||
# [DEF:get_param:Function]
|
||||
@@ -91,7 +97,8 @@ class TaskContext:
|
||||
# @PARAM: default (Any) - Default value if key not found.
|
||||
# @RETURN: Any - Parameter value or default.
|
||||
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:create_sub_context:Function]
|
||||
@@ -102,12 +109,13 @@ class TaskContext:
|
||||
# @RETURN: TaskContext - New context with different source.
|
||||
def create_sub_context(self, source: str) -> "TaskContext":
|
||||
"""Create a sub-context with a different default source for logging."""
|
||||
return TaskContext(
|
||||
task_id=self._task_id,
|
||||
add_log_fn=self._logger._add_log,
|
||||
params=self._params,
|
||||
default_source=source
|
||||
)
|
||||
with belief_scope("create_sub_context"):
|
||||
return TaskContext(
|
||||
task_id=self._task_id,
|
||||
add_log_fn=self._logger._add_log,
|
||||
params=self._params,
|
||||
default_source=source
|
||||
)
|
||||
# [/DEF:create_sub_context:Function]
|
||||
|
||||
# [/DEF:TaskContext:Class]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# [DEF:TaskManagerModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @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.
|
||||
# @LAYER: Core
|
||||
@@ -74,9 +75,10 @@ class TaskManager:
|
||||
# @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.
|
||||
def _flusher_loop(self):
|
||||
"""Background thread that flushes log buffer to database."""
|
||||
while not self._flusher_stop_event.is_set():
|
||||
self._flush_logs()
|
||||
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
|
||||
with belief_scope("_flusher_loop"):
|
||||
while not self._flusher_stop_event.is_set():
|
||||
self._flush_logs()
|
||||
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
|
||||
# [/DEF:_flusher_loop:Function]
|
||||
|
||||
# [DEF:_flush_logs:Function]
|
||||
@@ -85,23 +87,24 @@ class TaskManager:
|
||||
# @POST: All buffered logs are written to task_logs table.
|
||||
def _flush_logs(self):
|
||||
"""Flush all buffered logs to the database."""
|
||||
with self._log_buffer_lock:
|
||||
task_ids = list(self._log_buffer.keys())
|
||||
|
||||
for task_id in task_ids:
|
||||
with belief_scope("_flush_logs"):
|
||||
with self._log_buffer_lock:
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
task_ids = list(self._log_buffer.keys())
|
||||
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
# 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)
|
||||
for task_id in task_ids:
|
||||
with self._log_buffer_lock:
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
# 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_task_logs:Function]
|
||||
@@ -111,14 +114,15 @@ class TaskManager:
|
||||
# @PARAM: task_id (str) - The task ID.
|
||||
def _flush_task_logs(self, task_id: str):
|
||||
"""Flush logs for a specific task immediately."""
|
||||
with self._log_buffer_lock:
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
with belief_scope("_flush_task_logs"):
|
||||
with self._log_buffer_lock:
|
||||
logs = self._log_buffer.pop(task_id, [])
|
||||
|
||||
if logs:
|
||||
try:
|
||||
self.log_persistence_service.add_logs(task_id, logs)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flush logs for task {task_id}: {e}")
|
||||
# [/DEF:_flush_task_logs:Function]
|
||||
|
||||
# [DEF:create_task:Function]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# [DEF:TaskPersistenceModule:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage
|
||||
# @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database.
|
||||
# @LAYER: Core
|
||||
@@ -19,42 +20,65 @@ from ..logger import logger, belief_scope
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:TaskPersistenceService:Class]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: persistence, service, database, 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:
|
||||
# [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
|
||||
def _json_load_if_needed(value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped == "" or stripped.lower() == "null":
|
||||
with belief_scope("TaskPersistenceService._json_load_if_needed"):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
if isinstance(value, (dict, list)):
|
||||
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
|
||||
def _parse_datetime(value):
|
||||
if value is None or isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_environment_id(session: Session, env_id: Optional[str]) -> Optional[str]:
|
||||
if not env_id:
|
||||
with belief_scope("TaskPersistenceService._parse_datetime"):
|
||||
if value is None or isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
exists = session.query(Environment.id).filter(Environment.id == env_id).first()
|
||||
return env_id if exists else None
|
||||
# [/DEF:_parse_datetime:Function]
|
||||
|
||||
# [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]
|
||||
# @PURPOSE: Initializes the persistence service.
|
||||
@@ -90,13 +114,14 @@ class TaskPersistenceService:
|
||||
|
||||
# Ensure params and result are JSON serializable
|
||||
def json_serializable(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: json_serializable(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [json_serializable(v) for v in obj]
|
||||
elif isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
return obj
|
||||
with belief_scope("TaskPersistenceService.json_serializable"):
|
||||
if isinstance(obj, dict):
|
||||
return {k: json_serializable(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [json_serializable(v) for v in obj]
|
||||
elif isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
return obj
|
||||
|
||||
record.params = json_serializable(task.params)
|
||||
record.result = json_serializable(task.result)
|
||||
@@ -227,9 +252,11 @@ class TaskLogPersistenceService:
|
||||
"""
|
||||
|
||||
# [DEF:__init__:Function]
|
||||
# @PURPOSE: Initialize the log persistence service.
|
||||
# @POST: Service is ready.
|
||||
def __init__(self):
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Initializes the TaskLogPersistenceService
|
||||
# @PRE: config is provided or defaults are used
|
||||
# @POST: Service is ready for log persistence
|
||||
def __init__(self, config=None):
|
||||
pass
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
|
||||
@@ -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.
|
||||
# @TIER: CRITICAL
|
||||
# @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)
|
||||
class TaskLogger:
|
||||
"""
|
||||
@@ -71,6 +72,7 @@ class TaskLogger:
|
||||
# @PARAM: message (str) - Log message.
|
||||
# @PARAM: source (Optional[str]) - Override source for this log entry.
|
||||
# @PARAM: metadata (Optional[Dict]) - Additional structured data.
|
||||
# @UX_STATE: Logging -> (writing internal log)
|
||||
def _log(
|
||||
self,
|
||||
level: str,
|
||||
@@ -90,6 +92,8 @@ class TaskLogger:
|
||||
|
||||
# [DEF:debug:Function]
|
||||
# @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: source (Optional[str]) - Override source.
|
||||
# @PARAM: metadata (Optional[Dict]) - Additional data.
|
||||
@@ -104,6 +108,8 @@ class TaskLogger:
|
||||
|
||||
# [DEF:info:Function]
|
||||
# @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: source (Optional[str]) - Override source.
|
||||
# @PARAM: metadata (Optional[Dict]) - Additional data.
|
||||
@@ -118,6 +124,8 @@ class TaskLogger:
|
||||
|
||||
# [DEF:warning:Function]
|
||||
# @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: source (Optional[str]) - Override source.
|
||||
# @PARAM: metadata (Optional[Dict]) - Additional data.
|
||||
@@ -132,6 +140,8 @@ class TaskLogger:
|
||||
|
||||
# [DEF:error:Function]
|
||||
# @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: source (Optional[str]) - Override source.
|
||||
# @PARAM: metadata (Optional[Dict]) - Additional data.
|
||||
|
||||
@@ -16,6 +16,9 @@ from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
|
||||
# [DEF:TaskType:Class]
|
||||
# @TIER: CRITICAL
|
||||
# @INVARIANT: Must contain valid generic task type mappings.
|
||||
# @SEMANTICS: enum, type, task
|
||||
# @PURPOSE: Supported normalized task report types.
|
||||
class TaskType(str, Enum):
|
||||
LLM_VERIFICATION = "llm_verification"
|
||||
@@ -27,6 +30,9 @@ class TaskType(str, Enum):
|
||||
|
||||
|
||||
# [DEF:ReportStatus:Class]
|
||||
# @TIER: CRITICAL
|
||||
# @INVARIANT: TaskStatus enum mapping logic holds.
|
||||
# @SEMANTICS: enum, status, task
|
||||
# @PURPOSE: Supported normalized report status values.
|
||||
class ReportStatus(str, Enum):
|
||||
SUCCESS = "success"
|
||||
@@ -37,6 +43,9 @@ class ReportStatus(str, Enum):
|
||||
|
||||
|
||||
# [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.
|
||||
class ErrorContext(BaseModel):
|
||||
code: Optional[str] = None
|
||||
@@ -46,6 +55,9 @@ class ErrorContext(BaseModel):
|
||||
|
||||
|
||||
# [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.
|
||||
class TaskReport(BaseModel):
|
||||
report_id: str
|
||||
@@ -69,6 +81,9 @@ class TaskReport(BaseModel):
|
||||
|
||||
|
||||
# [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.
|
||||
class ReportQuery(BaseModel):
|
||||
page: int = Field(default=1, ge=1)
|
||||
@@ -105,6 +120,9 @@ class ReportQuery(BaseModel):
|
||||
|
||||
|
||||
# [DEF:ReportCollection:Class]
|
||||
# @TIER: CRITICAL
|
||||
# @INVARIANT: Represents paginated data correctly.
|
||||
# @SEMANTICS: collection, pagination
|
||||
# @PURPOSE: Paginated collection of normalized task reports.
|
||||
class ReportCollection(BaseModel):
|
||||
items: List[TaskReport]
|
||||
@@ -117,6 +135,9 @@ class ReportCollection(BaseModel):
|
||||
|
||||
|
||||
# [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.
|
||||
class ReportDetailView(BaseModel):
|
||||
report: TaskReport
|
||||
|
||||
@@ -33,7 +33,8 @@ class EncryptionManager:
|
||||
# @PRE: data must be a non-empty string.
|
||||
# @POST: Returns encrypted string.
|
||||
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.decrypt:Function]
|
||||
@@ -41,7 +42,8 @@ class EncryptionManager:
|
||||
# @PRE: encrypted_data must be a valid Fernet-encrypted string.
|
||||
# @POST: Returns original plaintext string.
|
||||
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:Class]
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ...core.logger import belief_scope
|
||||
from ...core.task_manager.models import Task, TaskStatus
|
||||
from ...models.report import ErrorContext, ReportStatus, TaskReport
|
||||
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.
|
||||
# @RETURN: ReportStatus - Canonical report status.
|
||||
def status_to_report_status(status: Any) -> ReportStatus:
|
||||
raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
|
||||
if raw == TaskStatus.SUCCESS.value:
|
||||
return ReportStatus.SUCCESS
|
||||
if raw == TaskStatus.FAILED.value:
|
||||
return ReportStatus.FAILED
|
||||
if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:
|
||||
return ReportStatus.IN_PROGRESS
|
||||
return ReportStatus.PARTIAL
|
||||
with belief_scope("status_to_report_status"):
|
||||
raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
|
||||
if raw == TaskStatus.SUCCESS.value:
|
||||
return ReportStatus.SUCCESS
|
||||
if raw == TaskStatus.FAILED.value:
|
||||
return ReportStatus.FAILED
|
||||
if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:
|
||||
return ReportStatus.IN_PROGRESS
|
||||
return ReportStatus.PARTIAL
|
||||
# [/DEF:status_to_report_status:Function]
|
||||
|
||||
|
||||
@@ -44,19 +46,20 @@ def status_to_report_status(status: Any) -> ReportStatus:
|
||||
# @PARAM: report_status (ReportStatus) - Canonical status.
|
||||
# @RETURN: str - Normalized summary.
|
||||
def build_summary(task: Task, report_status: ReportStatus) -> str:
|
||||
result = task.result
|
||||
if isinstance(result, dict):
|
||||
for key in ("summary", "message", "status_message", "description"):
|
||||
value = result.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
if report_status == ReportStatus.SUCCESS:
|
||||
return "Task completed successfully"
|
||||
if report_status == ReportStatus.FAILED:
|
||||
return "Task failed"
|
||||
if report_status == ReportStatus.IN_PROGRESS:
|
||||
return "Task is in progress"
|
||||
return "Task completed with partial data"
|
||||
with belief_scope("build_summary"):
|
||||
result = task.result
|
||||
if isinstance(result, dict):
|
||||
for key in ("summary", "message", "status_message", "description"):
|
||||
value = result.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
if report_status == ReportStatus.SUCCESS:
|
||||
return "Task completed successfully"
|
||||
if report_status == ReportStatus.FAILED:
|
||||
return "Task failed"
|
||||
if report_status == ReportStatus.IN_PROGRESS:
|
||||
return "Task is in progress"
|
||||
return "Task completed with partial data"
|
||||
# [/DEF:build_summary:Function]
|
||||
|
||||
|
||||
@@ -68,38 +71,39 @@ def build_summary(task: Task, report_status: ReportStatus) -> str:
|
||||
# @PARAM: report_status (ReportStatus) - Canonical status.
|
||||
# @RETURN: Optional[ErrorContext] - Error context block.
|
||||
def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:
|
||||
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
|
||||
return None
|
||||
with belief_scope("extract_error_context"):
|
||||
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
|
||||
return None
|
||||
|
||||
result = task.result if isinstance(task.result, dict) else {}
|
||||
message = None
|
||||
code = None
|
||||
next_actions = []
|
||||
result = task.result if isinstance(task.result, dict) else {}
|
||||
message = None
|
||||
code = None
|
||||
next_actions = []
|
||||
|
||||
if isinstance(result.get("error"), dict):
|
||||
error_obj = result.get("error", {})
|
||||
message = error_obj.get("message") or message
|
||||
code = error_obj.get("code") or code
|
||||
actions = error_obj.get("next_actions")
|
||||
if isinstance(actions, list):
|
||||
next_actions = [str(action) for action in actions if str(action).strip()]
|
||||
if isinstance(result.get("error"), dict):
|
||||
error_obj = result.get("error", {})
|
||||
message = error_obj.get("message") or message
|
||||
code = error_obj.get("code") or code
|
||||
actions = error_obj.get("next_actions")
|
||||
if isinstance(actions, list):
|
||||
next_actions = [str(action) for action in actions if str(action).strip()]
|
||||
|
||||
if not message:
|
||||
message = result.get("error_message") if isinstance(result.get("error_message"), str) else None
|
||||
if not message:
|
||||
message = result.get("error_message") if isinstance(result.get("error_message"), str) else None
|
||||
|
||||
if not message:
|
||||
for log in reversed(task.logs):
|
||||
if str(log.level).upper() == "ERROR" and log.message:
|
||||
message = log.message
|
||||
break
|
||||
if not message:
|
||||
for log in reversed(task.logs):
|
||||
if str(log.level).upper() == "ERROR" and log.message:
|
||||
message = log.message
|
||||
break
|
||||
|
||||
if not message:
|
||||
message = "Not provided"
|
||||
if not message:
|
||||
message = "Not provided"
|
||||
|
||||
if not next_actions:
|
||||
next_actions = ["Review task diagnostics", "Retry the operation"]
|
||||
if not next_actions:
|
||||
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]
|
||||
|
||||
|
||||
@@ -110,43 +114,44 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[E
|
||||
# @PARAM: task (Task) - Source task.
|
||||
# @RETURN: TaskReport - Canonical normalized report.
|
||||
def normalize_task_report(task: Task) -> TaskReport:
|
||||
task_type = resolve_task_type(task.plugin_id)
|
||||
report_status = status_to_report_status(task.status)
|
||||
profile = get_type_profile(task_type)
|
||||
with belief_scope("normalize_task_report"):
|
||||
task_type = resolve_task_type(task.plugin_id)
|
||||
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
|
||||
updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None
|
||||
if not updated_at:
|
||||
updated_at = started_at or datetime.utcnow()
|
||||
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
|
||||
if not updated_at:
|
||||
updated_at = started_at or datetime.utcnow()
|
||||
|
||||
details: Dict[str, Any] = {
|
||||
"profile": {
|
||||
"display_label": profile.get("display_label"),
|
||||
"visual_variant": profile.get("visual_variant"),
|
||||
"icon_token": profile.get("icon_token"),
|
||||
"emphasis_rules": profile.get("emphasis_rules", []),
|
||||
},
|
||||
"result": task.result if task.result is not None else {"note": "Not provided"},
|
||||
}
|
||||
details: Dict[str, Any] = {
|
||||
"profile": {
|
||||
"display_label": profile.get("display_label"),
|
||||
"visual_variant": profile.get("visual_variant"),
|
||||
"icon_token": profile.get("icon_token"),
|
||||
"emphasis_rules": profile.get("emphasis_rules", []),
|
||||
},
|
||||
"result": task.result if task.result is not None else {"note": "Not provided"},
|
||||
}
|
||||
|
||||
source_ref: Dict[str, Any] = {}
|
||||
if isinstance(task.params, dict):
|
||||
for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"):
|
||||
if key in task.params:
|
||||
source_ref[key] = task.params.get(key)
|
||||
source_ref: Dict[str, Any] = {}
|
||||
if isinstance(task.params, dict):
|
||||
for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"):
|
||||
if key in task.params:
|
||||
source_ref[key] = task.params.get(key)
|
||||
|
||||
return TaskReport(
|
||||
report_id=task.id,
|
||||
task_id=task.id,
|
||||
task_type=task_type,
|
||||
status=report_status,
|
||||
started_at=started_at,
|
||||
updated_at=updated_at,
|
||||
summary=build_summary(task, report_status),
|
||||
details=details,
|
||||
error_context=extract_error_context(task, report_status),
|
||||
source_ref=source_ref or None,
|
||||
)
|
||||
return TaskReport(
|
||||
report_id=task.id,
|
||||
task_id=task.id,
|
||||
task_type=task_type,
|
||||
status=report_status,
|
||||
started_at=started_at,
|
||||
updated_at=updated_at,
|
||||
summary=build_summary(task, report_status),
|
||||
details=details,
|
||||
error_context=extract_error_context(task, report_status),
|
||||
source_ref=source_ref or None,
|
||||
)
|
||||
# [/DEF:normalize_task_report:Function]
|
||||
|
||||
# [/DEF:backend.src.services.reports.normalizer:Module]
|
||||
@@ -12,6 +12,8 @@
|
||||
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
|
||||
@@ -33,7 +35,8 @@ class ReportsService:
|
||||
# @INVARIANT: Constructor performs no task mutations.
|
||||
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
|
||||
def __init__(self, task_manager: TaskManager):
|
||||
self.task_manager = task_manager
|
||||
with belief_scope("__init__"):
|
||||
self.task_manager = task_manager
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
# [DEF:_load_normalized_reports:Function]
|
||||
@@ -43,9 +46,10 @@ class ReportsService:
|
||||
# @INVARIANT: Every returned item is a TaskReport.
|
||||
# @RETURN: List[TaskReport] - Reports sorted later by list logic.
|
||||
def _load_normalized_reports(self) -> List[TaskReport]:
|
||||
tasks = self.task_manager.get_all_tasks()
|
||||
reports = [normalize_task_report(task) for task in tasks]
|
||||
return reports
|
||||
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]
|
||||
@@ -56,11 +60,12 @@ class ReportsService:
|
||||
# @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]:
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=timezone.utc)
|
||||
return value.astimezone(timezone.utc)
|
||||
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]
|
||||
@@ -71,10 +76,11 @@ class ReportsService:
|
||||
# @PARAM: report (TaskReport) - Report item.
|
||||
# @RETURN: float - UTC timestamp key.
|
||||
def _datetime_sort_key(self, report: TaskReport) -> float:
|
||||
updated = self._to_utc_datetime(report.updated_at)
|
||||
if updated is None:
|
||||
return 0.0
|
||||
return updated.timestamp()
|
||||
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]
|
||||
@@ -86,24 +92,25 @@ class ReportsService:
|
||||
# @PARAM: query (ReportQuery) - Applied query.
|
||||
# @RETURN: bool - True if report matches all filters.
|
||||
def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
|
||||
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:
|
||||
with belief_scope("_matches_query"):
|
||||
if query.task_types and report.task_type not in query.task_types:
|
||||
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:_sort_reports:Function]
|
||||
@@ -115,16 +122,17 @@ class ReportsService:
|
||||
# @PARAM: query (ReportQuery) - Sort config.
|
||||
# @RETURN: List[TaskReport] - Sorted reports.
|
||||
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":
|
||||
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)
|
||||
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
|
||||
return reports
|
||||
# [/DEF:_sort_reports:Function]
|
||||
|
||||
# [DEF:list_reports:Function]
|
||||
@@ -134,24 +142,25 @@ class ReportsService:
|
||||
# @PARAM: query (ReportQuery) - List filters and pagination.
|
||||
# @RETURN: ReportCollection - Paginated unified reports payload.
|
||||
def list_reports(self, query: ReportQuery) -> ReportCollection:
|
||||
reports = self._load_normalized_reports()
|
||||
filtered = [report for report in reports if self._matches_query(report, query)]
|
||||
sorted_reports = self._sort_reports(filtered, query)
|
||||
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
|
||||
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,
|
||||
)
|
||||
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]
|
||||
@@ -161,34 +170,35 @@ class ReportsService:
|
||||
# @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]:
|
||||
reports = self._load_normalized_reports()
|
||||
target = next((report for report in reports if report.report_id == report_id), None)
|
||||
if not target:
|
||||
return None
|
||||
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()})
|
||||
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()
|
||||
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"]
|
||||
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,
|
||||
)
|
||||
return ReportDetailView(
|
||||
report=target,
|
||||
timeline=timeline,
|
||||
diagnostics=diagnostics,
|
||||
next_actions=next_actions,
|
||||
)
|
||||
# [/DEF:get_report_detail:Function]
|
||||
# [/DEF:ReportsService:Class]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# [SECTION: IMPORTS]
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ...core.logger import belief_scope
|
||||
from ...models.report import TaskType
|
||||
# [/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.
|
||||
# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.
|
||||
def resolve_task_type(plugin_id: Optional[str]) -> TaskType:
|
||||
normalized = (plugin_id or "").strip()
|
||||
if not normalized:
|
||||
return TaskType.UNKNOWN
|
||||
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)
|
||||
with belief_scope("resolve_task_type"):
|
||||
normalized = (plugin_id or "").strip()
|
||||
if not normalized:
|
||||
return TaskType.UNKNOWN
|
||||
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)
|
||||
# [/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.
|
||||
# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.
|
||||
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:backend.src.services.reports.type_profiles:Module]
|
||||
@@ -12,6 +12,8 @@
|
||||
/**
|
||||
* @TIER CRITICAL
|
||||
* @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 Streaming -> Displays logs with auto-scroll, real-time appending
|
||||
* @UX_STATE Error -> Shows error message with recovery option
|
||||
@@ -42,6 +44,9 @@
|
||||
let shouldShow = $derived(inline || show);
|
||||
|
||||
// [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(() => {
|
||||
if (realTimeLogs && realTimeLogs.length > 0) {
|
||||
const lastLog = realTimeLogs[realTimeLogs.length - 1];
|
||||
@@ -58,11 +63,20 @@
|
||||
// [/DEF:handleRealTimeLogs:Action]
|
||||
|
||||
// [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() {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
console.log(`[TaskLogViewer][API][fetchLogs:STARTED] id=${taskId}`);
|
||||
logs = await getTaskLogs(taskId);
|
||||
console.log(`[TaskLogViewer][API][fetchLogs:SUCCESS] id=${taskId}`);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[TaskLogViewer][API][fetchLogs:FAILED] id=${taskId}`,
|
||||
e,
|
||||
);
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -70,13 +84,25 @@
|
||||
}
|
||||
// [/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) {
|
||||
console.log("[TaskLogViewer][UI][handleFilterChange:START]");
|
||||
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() {
|
||||
console.log("[TaskLogViewer][UI][handleRefresh:START]");
|
||||
fetchLogs();
|
||||
}
|
||||
// [/DEF:handleRefresh:Function]
|
||||
|
||||
$effect(() => {
|
||||
if (shouldShow && taskId) {
|
||||
@@ -104,6 +130,11 @@
|
||||
</script>
|
||||
|
||||
{#if shouldShow}
|
||||
<!-- [DEF:showInline:Component] -->
|
||||
<!-- @PURPOSE: Shows inline logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, inline -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
{#if inline}
|
||||
<div class="flex flex-col h-full w-full">
|
||||
{#if loading && logs.length === 0}
|
||||
@@ -136,7 +167,13 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:showInline:Component] -->
|
||||
{:else}
|
||||
<!-- [DEF:showModal:Component] -->
|
||||
<!-- @PURPOSE: Shows modal logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, modal -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
@@ -199,5 +236,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
// [/DEF:showModal:Component]
|
||||
|
||||
<!-- [/DEF:TaskLogViewer:Component] -->
|
||||
|
||||
@@ -13,6 +13,7 @@ import { api } from '../api.js';
|
||||
// @PRE: options is an object with optional report query fields.
|
||||
// @POST: Returns URL query string without leading '?'.
|
||||
export function buildReportQueryString(options = {}) {
|
||||
console.log("[reports][api][buildReportQueryString:START]");
|
||||
const params = new URLSearchParams();
|
||||
|
||||
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.
|
||||
// @POST: Returns structured error object.
|
||||
export function normalizeApiError(error) {
|
||||
console.log("[reports][api][normalizeApiError:START]");
|
||||
const message =
|
||||
(error && typeof error.message === 'string' && error.message) ||
|
||||
(typeof error === 'string' && error) ||
|
||||
@@ -59,9 +61,13 @@ export function normalizeApiError(error) {
|
||||
// @POST: Returns parsed payload or structured error for UI-state mapping.
|
||||
export async function getReports(options = {}) {
|
||||
try {
|
||||
console.log("[reports][api][getReports:STARTED]", 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) {
|
||||
console.error("[reports][api][getReports:FAILED]", error);
|
||||
throw normalizeApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -73,8 +79,12 @@ export async function getReports(options = {}) {
|
||||
// @POST: Returns parsed detail payload or structured error object.
|
||||
export async function getReportDetail(reportId) {
|
||||
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) {
|
||||
console.error(`[reports][api][getReportDetail:FAILED] id=${reportId}`, error);
|
||||
throw normalizeApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,35 +23,35 @@
|
||||
* @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id}
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n';
|
||||
import Icon from '$lib/ui/Icon.svelte';
|
||||
import { openDrawerForTask } from '$lib/stores/taskDrawer.js';
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { t } from "$lib/i18n";
|
||||
import Icon from "$lib/ui/Icon.svelte";
|
||||
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
|
||||
import {
|
||||
assistantChatStore,
|
||||
closeAssistantChat,
|
||||
setAssistantConversationId,
|
||||
} from '$lib/stores/assistantChat.js';
|
||||
} from "$lib/stores/assistantChat.js";
|
||||
import {
|
||||
sendAssistantMessage,
|
||||
confirmAssistantOperation,
|
||||
cancelAssistantOperation,
|
||||
getAssistantHistory,
|
||||
getAssistantConversations,
|
||||
} from '$lib/api/assistant.js';
|
||||
} from "$lib/api/assistant.js";
|
||||
|
||||
const HISTORY_PAGE_SIZE = 30;
|
||||
const CONVERSATIONS_PAGE_SIZE = 20;
|
||||
|
||||
let input = '';
|
||||
let input = "";
|
||||
let loading = false;
|
||||
let loadingHistory = false;
|
||||
let loadingMoreHistory = false;
|
||||
let loadingConversations = false;
|
||||
let messages = [];
|
||||
let conversations = [];
|
||||
let conversationFilter = 'active';
|
||||
let conversationFilter = "active";
|
||||
let activeConversationsTotal = 0;
|
||||
let archivedConversationsTotal = 0;
|
||||
let historyPage = 1;
|
||||
@@ -77,7 +77,12 @@
|
||||
const requestVersion = ++historyLoadVersion;
|
||||
loadingHistory = true;
|
||||
try {
|
||||
const history = await getAssistantHistory(1, HISTORY_PAGE_SIZE, targetConversationId, true);
|
||||
const history = await getAssistantHistory(
|
||||
1,
|
||||
HISTORY_PAGE_SIZE,
|
||||
targetConversationId,
|
||||
true,
|
||||
);
|
||||
if (requestVersion !== historyLoadVersion) {
|
||||
return;
|
||||
}
|
||||
@@ -87,13 +92,21 @@
|
||||
}));
|
||||
historyPage = 1;
|
||||
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);
|
||||
}
|
||||
initialized = true;
|
||||
console.log('[AssistantChatPanel][Coherence:OK] History loaded');
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][history][loadHistory:SUCCESS] History loaded");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load history', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][history][loadHistory:FAILED] Failed to load history",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingHistory = false;
|
||||
}
|
||||
@@ -111,23 +124,30 @@
|
||||
loadingConversations = true;
|
||||
try {
|
||||
const page = reset ? 1 : conversationsPage + 1;
|
||||
const includeArchived = conversationFilter === 'archived';
|
||||
const archivedOnly = conversationFilter === 'archived';
|
||||
const includeArchived = conversationFilter === "archived";
|
||||
const archivedOnly = conversationFilter === "archived";
|
||||
const response = await getAssistantConversations(
|
||||
page,
|
||||
CONVERSATIONS_PAGE_SIZE,
|
||||
includeArchived,
|
||||
'',
|
||||
"",
|
||||
archivedOnly,
|
||||
);
|
||||
const rows = response.items || [];
|
||||
conversations = reset ? rows : [...conversations, ...rows];
|
||||
conversationsPage = page;
|
||||
conversationsHasNext = Boolean(response.has_next);
|
||||
activeConversationsTotal = response.active_total ?? activeConversationsTotal;
|
||||
archivedConversationsTotal = response.archived_total ?? archivedConversationsTotal;
|
||||
activeConversationsTotal =
|
||||
response.active_total ?? activeConversationsTotal;
|
||||
archivedConversationsTotal =
|
||||
response.archived_total ?? archivedConversationsTotal;
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversations][loadConversations:SUCCESS]");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load conversations', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][conversations][loadConversations:FAILED]",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingConversations = false;
|
||||
}
|
||||
@@ -141,11 +161,22 @@
|
||||
* @POST: Older messages are prepended while preserving order.
|
||||
*/
|
||||
async function loadOlderMessages() {
|
||||
if (loadingMoreHistory || loadingHistory || !historyHasNext || !conversationId) return;
|
||||
if (
|
||||
loadingMoreHistory ||
|
||||
loadingHistory ||
|
||||
!historyHasNext ||
|
||||
!conversationId
|
||||
)
|
||||
return;
|
||||
loadingMoreHistory = true;
|
||||
try {
|
||||
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) => ({
|
||||
...msg,
|
||||
actions: msg.actions || msg.metadata?.actions || [],
|
||||
@@ -155,8 +186,12 @@
|
||||
messages = [...uniqueChunk, ...messages];
|
||||
historyPage = nextPage;
|
||||
historyHasNext = Boolean(history.has_next);
|
||||
console.log("[AssistantChatPanel][history][loadOlderMessages:SUCCESS]");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load older messages', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][history][loadOlderMessages:FAILED]",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingMoreHistory = false;
|
||||
}
|
||||
@@ -170,7 +205,9 @@
|
||||
|
||||
$: if (isOpen && initialized && conversationId) {
|
||||
// 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) {
|
||||
loadHistory();
|
||||
}
|
||||
@@ -183,11 +220,12 @@
|
||||
* @POST: user message appears at the end of messages list.
|
||||
*/
|
||||
function appendLocalUserMessage(text) {
|
||||
console.log("[AssistantChatPanel][message][appendLocalUserMessage][START]");
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: `local-${Date.now()}`,
|
||||
role: 'user',
|
||||
role: "user",
|
||||
text,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
@@ -202,11 +240,13 @@
|
||||
* @POST: assistant message appended with state/task/actions metadata.
|
||||
*/
|
||||
function appendAssistantResponse(response) {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][message][appendAssistantResponse][START]");
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: response.response_id,
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
text: response.text,
|
||||
state: response.state,
|
||||
task_id: response.task_id || null,
|
||||
@@ -220,12 +260,12 @@
|
||||
|
||||
function buildConversationTitle(conversation) {
|
||||
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)}`;
|
||||
}
|
||||
|
||||
function setConversationFilter(filter) {
|
||||
if (filter !== 'active' && filter !== 'archived') return;
|
||||
if (filter !== "active" && filter !== "archived") return;
|
||||
if (conversationFilter === filter) return;
|
||||
conversationFilter = filter;
|
||||
conversations = [];
|
||||
@@ -235,9 +275,9 @@
|
||||
}
|
||||
|
||||
function formatConversationTime(iso) {
|
||||
if (!iso) return '';
|
||||
if (!iso) return "";
|
||||
const dt = new Date(iso);
|
||||
if (Number.isNaN(dt.getTime())) return '';
|
||||
if (Number.isNaN(dt.getTime())) return "";
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
@@ -249,11 +289,12 @@
|
||||
* @SIDE_EFFECT: Triggers backend command execution pipeline.
|
||||
*/
|
||||
async function handleSend() {
|
||||
console.log("[AssistantChatPanel][message][handleSend][START]");
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
|
||||
appendLocalUserMessage(text);
|
||||
input = '';
|
||||
input = "";
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
@@ -271,8 +312,8 @@
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `error-${Date.now()}`,
|
||||
text: err.message || 'Assistant request failed',
|
||||
state: 'failed',
|
||||
text: err.message || "Assistant request failed",
|
||||
state: "failed",
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
@@ -289,6 +330,8 @@
|
||||
* @POST: conversationId updated and history reloaded.
|
||||
*/
|
||||
async function selectConversation(conversation) {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversation][selectConversation][START]");
|
||||
if (!conversation?.conversation_id) return;
|
||||
if (conversation.conversation_id === conversationId) return;
|
||||
// Invalidate any in-flight history request to avoid stale conversation overwrite.
|
||||
@@ -308,8 +351,10 @@
|
||||
* @POST: Messages reset and new conversation id bound.
|
||||
*/
|
||||
function startNewConversation() {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversation][startNewConversation][START]");
|
||||
const newId =
|
||||
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
||||
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `conv-${Date.now()}`;
|
||||
setAssistantConversationId(newId);
|
||||
@@ -328,32 +373,37 @@
|
||||
* @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints.
|
||||
*/
|
||||
async function handleAction(action, message) {
|
||||
console.log("[AssistantChatPanel][action][handleAction][START]");
|
||||
try {
|
||||
if (action.type === 'open_task' && action.target) {
|
||||
if (action.type === "open_task" && action.target) {
|
||||
openDrawerForTask(action.target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'open_reports') {
|
||||
goto('/reports');
|
||||
if (action.type === "open_reports") {
|
||||
goto("/reports");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'confirm' && message.confirmation_id) {
|
||||
const response = await confirmAssistantOperation(message.confirmation_id);
|
||||
if (action.type === "confirm" && message.confirmation_id) {
|
||||
const response = await confirmAssistantOperation(
|
||||
message.confirmation_id,
|
||||
);
|
||||
appendAssistantResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'cancel' && message.confirmation_id) {
|
||||
const response = await cancelAssistantOperation(message.confirmation_id);
|
||||
if (action.type === "cancel" && message.confirmation_id) {
|
||||
const response = await cancelAssistantOperation(
|
||||
message.confirmation_id,
|
||||
);
|
||||
appendAssistantResponse(response);
|
||||
}
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `action-error-${Date.now()}`,
|
||||
text: err.message || 'Action failed',
|
||||
state: 'failed',
|
||||
text: err.message || "Action failed",
|
||||
state: "failed",
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
@@ -368,7 +418,8 @@
|
||||
* @POST: handleSend is invoked when Enter is pressed without shift modifier.
|
||||
*/
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
console.log("[AssistantChatPanel][input][handleKeydown][START]");
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
@@ -382,12 +433,17 @@
|
||||
* @POST: Tailwind class string returned for badge rendering.
|
||||
*/
|
||||
function stateClass(state) {
|
||||
if (state === 'started') return 'bg-sky-100 text-sky-700 border-sky-200';
|
||||
if (state === 'success') return 'bg-emerald-100 text-emerald-700 border-emerald-200';
|
||||
if (state === 'needs_confirmation') 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';
|
||||
console.log("[AssistantChatPanel][ui][stateClass][START]");
|
||||
if (state === "started") return "bg-sky-100 text-sky-700 border-sky-200";
|
||||
if (state === "success")
|
||||
return "bg-emerald-100 text-emerald-700 border-emerald-200";
|
||||
if (state === "needs_confirmation")
|
||||
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]
|
||||
|
||||
@@ -398,8 +454,9 @@
|
||||
* @POST: loadOlderMessages called when boundary and more pages available.
|
||||
*/
|
||||
function handleHistoryScroll(event) {
|
||||
console.log("[AssistantChatPanel][scroll][handleHistoryScroll][START]");
|
||||
const el = event.currentTarget;
|
||||
if (!el || typeof el.scrollTop !== 'number') return;
|
||||
if (!el || typeof el.scrollTop !== "number") return;
|
||||
if (el.scrollTop <= 16) {
|
||||
loadOlderMessages();
|
||||
}
|
||||
@@ -412,18 +469,28 @@
|
||||
</script>
|
||||
|
||||
{#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">
|
||||
<div class="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
<button
|
||||
class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
|
||||
on:click={closeAssistantChat}
|
||||
aria-label={$t.assistant?.close || 'Close assistant'}
|
||||
aria-label={$t.assistant?.close || "Close assistant"}
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
@@ -432,7 +499,10 @@
|
||||
<div class="flex h-[calc(100%-56px)] flex-col">
|
||||
<div class="border-b border-slate-200 px-3 py-2">
|
||||
<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
|
||||
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}
|
||||
@@ -442,14 +512,20 @@
|
||||
</div>
|
||||
<div class="mb-2 flex items-center gap-1">
|
||||
<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'}"
|
||||
on:click={() => setConversationFilter('active')}
|
||||
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'}"
|
||||
on:click={() => setConversationFilter("active")}
|
||||
>
|
||||
Active ({activeConversationsTotal})
|
||||
</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'}"
|
||||
on:click={() => setConversationFilter('archived')}
|
||||
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'}"
|
||||
on:click={() => setConversationFilter("archived")}
|
||||
>
|
||||
Archived ({archivedConversationsTotal})
|
||||
</button>
|
||||
@@ -457,16 +533,27 @@
|
||||
<div class="flex gap-2 overflow-x-auto pb-1">
|
||||
{#each conversations as convo (convo.conversation_id)}
|
||||
<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)}
|
||||
title={formatConversationTime(convo.updated_at)}
|
||||
>
|
||||
<div class="truncate font-semibold">{buildConversationTitle(convo)}</div>
|
||||
<div class="truncate text-[10px] text-slate-500">{convo.last_message || ''}</div>
|
||||
<div class="truncate font-semibold">
|
||||
{buildConversationTitle(convo)}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-slate-500">
|
||||
{convo.last_message || ""}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#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 conversationsHasNext}
|
||||
<button
|
||||
@@ -479,15 +566,29 @@
|
||||
</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}
|
||||
<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 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}
|
||||
<div 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="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>• сделай ветку feature/new-dashboard для дашборда 42</div>
|
||||
<div>• запусти миграцию с dev на prod для дашборда 42</div>
|
||||
@@ -497,29 +598,44 @@
|
||||
{/if}
|
||||
|
||||
{#each messages as message (message.message_id)}
|
||||
<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={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="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
{message.role === 'user' ? 'You' : 'Assistant'}
|
||||
<span
|
||||
class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
{message.role === "user" ? "You" : "Assistant"}
|
||||
</span>
|
||||
{#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}
|
||||
</span>
|
||||
{/if}
|
||||
</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}
|
||||
<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
|
||||
class="text-xs font-medium text-sky-700 hover:text-sky-900"
|
||||
on:click={() => openDrawerForTask(message.task_id)}
|
||||
>
|
||||
{$t.assistant?.open_task_drawer || 'Open Task Drawer'}
|
||||
{$t.assistant?.open_task_drawer || "Open Task Drawer"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -544,12 +660,14 @@
|
||||
<div class="mr-8">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-3">
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
<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></span><span></span><span></span>
|
||||
</span>
|
||||
@@ -564,7 +682,7 @@
|
||||
<textarea
|
||||
bind:value={input}
|
||||
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"
|
||||
on:keydown={handleKeydown}
|
||||
></textarea>
|
||||
@@ -573,7 +691,7 @@
|
||||
on:click={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
>
|
||||
{loading ? '...' : ($t.assistant?.send || 'Send')}
|
||||
{loading ? "..." : $t.assistant?.send || "Send"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -581,6 +699,8 @@
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- [/DEF:AssistantChatPanel:Component] -->
|
||||
|
||||
<style>
|
||||
.thinking-dots {
|
||||
display: inline-flex;
|
||||
@@ -618,4 +738,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- [/DEF:AssistantChatPanel:Component] -->
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
// Close drawer
|
||||
function handleClose() {
|
||||
console.log("[TaskDrawer][Action] Close drawer");
|
||||
console.log("[TaskDrawer][ui][Close_drawer]");
|
||||
closeDrawer();
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("[TaskDrawer][WebSocket] Received message:", data);
|
||||
console.log(`[TaskDrawer][WebSocket][Message_Received] ${data.message}`);
|
||||
|
||||
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() {
|
||||
console.log("[TaskDrawer][WebSocket][disconnectWebSocket:START]");
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
// [/DEF:disconnectWebSocket:Function]
|
||||
|
||||
// [DEF:loadRecentTasks:Function]
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
async function loadRecentTasks() {
|
||||
loadingTasks = true;
|
||||
try {
|
||||
console.log("[TaskDrawer][API][loadRecentTasks:STARTED]");
|
||||
// API returns List[Task] directly, not {tasks: [...]}
|
||||
const response = await api.getTasks();
|
||||
recentTasks = Array.isArray(response) ? response : (response.tasks || []);
|
||||
console.log("[TaskDrawer][Action] Loaded recent tasks:", recentTasks.length);
|
||||
recentTasks = Array.isArray(response) ? response : response.tasks || [];
|
||||
console.log(
|
||||
`[TaskDrawer][API][loadRecentTasks:SUCCESS] loaded ${recentTasks.length} tasks`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[TaskDrawer][Coherence:Failed] Failed to load tasks:", err);
|
||||
console.error("[TaskDrawer][API][loadRecentTasks:FAILED]", err);
|
||||
recentTasks = [];
|
||||
} finally {
|
||||
loadingTasks = false;
|
||||
@@ -150,11 +162,14 @@
|
||||
// [DEF:selectTask:Function]
|
||||
/**
|
||||
* @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) {
|
||||
taskDrawerStore.update(state => ({
|
||||
console.log("[TaskDrawer][UI][selectTask:START]");
|
||||
taskDrawerStore.update((state) => ({
|
||||
...state,
|
||||
activeTaskId: task.id
|
||||
activeTaskId: task.id,
|
||||
}));
|
||||
}
|
||||
// [/DEF:selectTask:Function]
|
||||
@@ -162,14 +177,19 @@
|
||||
// [DEF:goBackToList:Function]
|
||||
/**
|
||||
* @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() {
|
||||
taskDrawerStore.update(state => ({
|
||||
console.log("[TaskDrawer][UI][goBackToList:START]");
|
||||
taskDrawerStore.update((state) => ({
|
||||
...state,
|
||||
activeTaskId: null
|
||||
activeTaskId: null,
|
||||
}));
|
||||
// Reload the task list
|
||||
loadRecentTasks();
|
||||
console.log("[TaskDrawer][UI][goBackToList:SUCCESS]");
|
||||
}
|
||||
// [/DEF:goBackToList:Function]
|
||||
|
||||
@@ -202,100 +222,145 @@
|
||||
aria-modal="false"
|
||||
aria-label={$t.tasks?.drawer || "Task drawer"}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if !activeTaskId && recentTasks.length > 0}
|
||||
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
||||
<Icon name="list" size={16} strokeWidth={2} />
|
||||
</span>
|
||||
{:else if activeTaskId}
|
||||
<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}
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if !activeTaskId && recentTasks.length > 0}
|
||||
<span
|
||||
class="flex items-center justify-center p-1.5 mr-1 text-cyan-400"
|
||||
>
|
||||
{$t.nav?.reports || "Reports"}
|
||||
</button>
|
||||
<Icon name="list" size={16} strokeWidth={2} />
|
||||
</span>
|
||||
{:else if activeTaskId}
|
||||
<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"}
|
||||
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="close" size={18} strokeWidth={2} />
|
||||
<Icon name="back" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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}
|
||||
<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>
|
||||
|
||||
<!-- 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 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"}
|
||||
</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>
|
||||
</aside>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/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}
|
||||
```
|
||||
|
||||
@@ -52,6 +52,8 @@ export const REPORT_TYPE_PROFILES = {
|
||||
// @POST: Returns one profile object.
|
||||
export function getReportTypeProfile(taskType) {
|
||||
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;
|
||||
}
|
||||
// [/DEF:getReportTypeProfile:Function]
|
||||
|
||||
123
gen_map_module.json
Normal file
123
gen_map_module.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
# [DEF:generate_semantic_map:Module]
|
||||
#
|
||||
# @TIER: CRITICAL
|
||||
# @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
|
||||
# @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.
|
||||
# @RELATION: READS -> FileSystem
|
||||
# @RELATION: PRODUCES -> semantics/semantic_map.json
|
||||
@@ -262,7 +263,7 @@ class SemanticEntity:
|
||||
# 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:
|
||||
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(
|
||||
f"Missing Belief State Logging: Function should use {log_type} (required for {tier.value} tier)",
|
||||
severity,
|
||||
@@ -296,13 +297,17 @@ class SemanticEntity:
|
||||
tier = self.get_tier()
|
||||
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
|
||||
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])
|
||||
|
||||
# Penalties
|
||||
score -= errors * 0.3
|
||||
score -= warnings * 0.1
|
||||
score -= errors * error_penalty
|
||||
score -= warnings * warning_penalty
|
||||
|
||||
# Check mandatory tags
|
||||
required = TIER_MANDATORY_TAGS.get(tier, {}).get(self.type, [])
|
||||
@@ -314,7 +319,8 @@ class SemanticEntity:
|
||||
found_count += 1
|
||||
break
|
||||
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)
|
||||
# [/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>.*)"),
|
||||
"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+)"),
|
||||
"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:
|
||||
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>.*)"),
|
||||
"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+)"),
|
||||
"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
|
||||
"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*\>"),
|
||||
@@ -609,8 +616,10 @@ def parse_file(full_path: str, rel_path: str, lang: str) -> Tuple[List[SemanticE
|
||||
current.tags[tag_name] = tag_value
|
||||
|
||||
# Check for belief scope in implementation
|
||||
if lang == "python" and "belief_scope" in patterns:
|
||||
if patterns["belief_scope"].search(line):
|
||||
if lang == "python":
|
||||
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
|
||||
|
||||
# Check for console.log belief state in Svelte
|
||||
@@ -803,26 +812,39 @@ class SemanticMapGenerator:
|
||||
with belief_scope("_process_file_results"):
|
||||
total_score = 0
|
||||
count = 0
|
||||
module_max_tier = Tier.TRIVIAL
|
||||
|
||||
# [DEF:validate_recursive:Function]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Recursively validates a list of entities.
|
||||
# @PRE: ent_list is a list of SemanticEntity objects.
|
||||
# @POST: All entities and their children are validated.
|
||||
# @PURPOSE: Calculate score and determine module's max tier for weighted global score
|
||||
# @PRE: Entities exist
|
||||
# @POST: Entities are validated
|
||||
def validate_recursive(ent_list):
|
||||
with belief_scope("validate_recursive"):
|
||||
nonlocal total_score, count
|
||||
nonlocal total_score, count, module_max_tier
|
||||
for e in ent_list:
|
||||
e.validate()
|
||||
total_score += e.get_score()
|
||||
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)
|
||||
# [/DEF:validate_recursive:Function]
|
||||
|
||||
validate_recursive(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:_generate_artifacts:Function]
|
||||
@@ -860,7 +882,19 @@ class SemanticMapGenerator:
|
||||
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||
|
||||
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
|
||||
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("|------|-------|------|--------|\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 = []
|
||||
tier = "N/A"
|
||||
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 "🔴"
|
||||
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.
|
||||
# @POST: Returns a module path string.
|
||||
def _get_module_path(file_path: str) -> str:
|
||||
# Convert file path to module-like path
|
||||
parts = file_path.replace(os.sep, '/').split('/')
|
||||
# Remove filename
|
||||
if len(parts) > 1:
|
||||
return '/'.join(parts[:-1])
|
||||
return 'root'
|
||||
with belief_scope("_get_module_path"):
|
||||
# Convert file path to module-like path
|
||||
parts = file_path.replace(os.sep, '/').split('/')
|
||||
# Remove filename
|
||||
if len(parts) > 1:
|
||||
return '/'.join(parts[:-1])
|
||||
return 'root'
|
||||
# [/DEF:_get_module_path:Function]
|
||||
|
||||
# [DEF:_collect_all_entities:Function]
|
||||
@@ -1038,9 +1080,10 @@ class SemanticMapGenerator:
|
||||
# @PRE: entity list is valid.
|
||||
# @POST: Returns flat list of all entities with their hierarchy.
|
||||
def _collect_all_entities(entities: List[SemanticEntity], result: List[Tuple[str, SemanticEntity]]):
|
||||
for e in entities:
|
||||
result.append((_get_module_path(e.file_path), e))
|
||||
_collect_all_entities(e.children, result)
|
||||
with belief_scope("_collect_all_entities"):
|
||||
for e in entities:
|
||||
result.append((_get_module_path(e.file_path), e))
|
||||
_collect_all_entities(e.children, result)
|
||||
# [/DEF:_collect_all_entities:Function]
|
||||
|
||||
# Collect all entities
|
||||
|
||||
0
pers_module.json
Normal file
0
pers_module.json
Normal file
File diff suppressed because it is too large
Load Diff
20
test_analyze.py
Normal file
20
test_analyze.py
Normal 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
25
test_parse.py
Normal 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
10
test_parse2.py
Normal 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
227
test_parser.py
Normal 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
13
test_regex.py
Normal 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()}")
|
||||
Reference in New Issue
Block a user