x-ipe 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- x_ipe/app.py +32 -1
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +289 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +10 -2
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +50 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +535 -2
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +37 -7
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +148 -1
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +23 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/uiux-feedback.css +7 -1
- x_ipe/static/css/workplace.css +636 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/confirm-dialog.js +169 -0
- x_ipe/static/js/features/folder-view.js +742 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-drag.js +227 -0
- x_ipe/static/js/features/tree-search.js +228 -0
- x_ipe/static/js/features/workplace.js +661 -33
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/terminal-v2.js +45 -14
- x_ipe/static/js/terminal.js +50 -49
- x_ipe/static/js/uiux-feedback.js +75 -16
- x_ipe/templates/base.html +24 -0
- x_ipe/templates/index.html +10 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
- x_ipe/app.py.bak +0 -1333
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,8 @@ from collections import deque
|
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from typing import Dict, Optional, Any, Callable
|
|
15
15
|
|
|
16
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
# Constants for session management
|
|
18
20
|
BUFFER_MAX_CHARS = 10240 # 10KB limit for output buffer
|
|
@@ -30,15 +32,18 @@ class OutputBuffer:
|
|
|
30
32
|
def __init__(self, max_chars: int = BUFFER_MAX_CHARS):
|
|
31
33
|
self._buffer: deque = deque(maxlen=max_chars)
|
|
32
34
|
|
|
35
|
+
@x_ipe_tracing()
|
|
33
36
|
def append(self, data: str) -> None:
|
|
34
37
|
"""Append data character by character to maintain limit."""
|
|
35
38
|
for char in data:
|
|
36
39
|
self._buffer.append(char)
|
|
37
40
|
|
|
41
|
+
@x_ipe_tracing()
|
|
38
42
|
def get_contents(self) -> str:
|
|
39
43
|
"""Get all buffered content as string."""
|
|
40
44
|
return ''.join(self._buffer)
|
|
41
45
|
|
|
46
|
+
@x_ipe_tracing()
|
|
42
47
|
def clear(self) -> None:
|
|
43
48
|
"""Clear the buffer."""
|
|
44
49
|
self._buffer.clear()
|
|
@@ -68,6 +73,7 @@ class PersistentSession:
|
|
|
68
73
|
self.created_at = datetime.now()
|
|
69
74
|
self._lock = threading.Lock()
|
|
70
75
|
|
|
76
|
+
@x_ipe_tracing()
|
|
71
77
|
def start_pty(self, rows: int = 24, cols: int = 80) -> None:
|
|
72
78
|
"""Start the underlying PTY process."""
|
|
73
79
|
def buffered_emit(data: str) -> None:
|
|
@@ -80,6 +86,7 @@ class PersistentSession:
|
|
|
80
86
|
self.pty_session = PTYSession(self.session_id, buffered_emit)
|
|
81
87
|
self.pty_session.start(rows, cols)
|
|
82
88
|
|
|
89
|
+
@x_ipe_tracing()
|
|
83
90
|
def attach(self, socket_sid: str, emit_callback: Callable[[str], None]) -> None:
|
|
84
91
|
"""Attach a WebSocket connection to this session."""
|
|
85
92
|
with self._lock:
|
|
@@ -88,6 +95,7 @@ class PersistentSession:
|
|
|
88
95
|
self.state = 'connected'
|
|
89
96
|
self.disconnect_time = None
|
|
90
97
|
|
|
98
|
+
@x_ipe_tracing()
|
|
91
99
|
def detach(self) -> None:
|
|
92
100
|
"""Detach WebSocket, keeping PTY alive for reconnection."""
|
|
93
101
|
with self._lock:
|
|
@@ -96,20 +104,24 @@ class PersistentSession:
|
|
|
96
104
|
self.state = 'disconnected'
|
|
97
105
|
self.disconnect_time = datetime.now()
|
|
98
106
|
|
|
107
|
+
@x_ipe_tracing()
|
|
99
108
|
def get_buffer(self) -> str:
|
|
100
109
|
"""Get buffered output for replay."""
|
|
101
110
|
return self.output_buffer.get_contents()
|
|
102
111
|
|
|
112
|
+
@x_ipe_tracing()
|
|
103
113
|
def write(self, data: str) -> None:
|
|
104
114
|
"""Write input to PTY."""
|
|
105
115
|
if self.pty_session:
|
|
106
116
|
self.pty_session.write(data)
|
|
107
117
|
|
|
118
|
+
@x_ipe_tracing()
|
|
108
119
|
def resize(self, rows: int, cols: int) -> None:
|
|
109
120
|
"""Resize the PTY."""
|
|
110
121
|
if self.pty_session:
|
|
111
122
|
self.pty_session._set_size(rows, cols)
|
|
112
123
|
|
|
124
|
+
@x_ipe_tracing()
|
|
113
125
|
def is_expired(self, timeout_seconds: int = SESSION_TIMEOUT) -> bool:
|
|
114
126
|
"""Check if session has expired (1hr after disconnect)."""
|
|
115
127
|
if self.state == 'connected':
|
|
@@ -119,6 +131,7 @@ class PersistentSession:
|
|
|
119
131
|
elapsed = datetime.now() - self.disconnect_time
|
|
120
132
|
return elapsed.total_seconds() > timeout_seconds
|
|
121
133
|
|
|
134
|
+
@x_ipe_tracing()
|
|
122
135
|
def close(self) -> None:
|
|
123
136
|
"""Close session and cleanup resources."""
|
|
124
137
|
if self.pty_session:
|
|
@@ -139,6 +152,7 @@ class SessionManager:
|
|
|
139
152
|
self._cleanup_timer: Optional[threading.Timer] = None
|
|
140
153
|
self._running = False
|
|
141
154
|
|
|
155
|
+
@x_ipe_tracing()
|
|
142
156
|
def create_session(self, emit_callback: Callable[[str], None],
|
|
143
157
|
rows: int = 24, cols: int = 80) -> str:
|
|
144
158
|
"""Create new persistent session, returns session_id."""
|
|
@@ -152,16 +166,19 @@ class SessionManager:
|
|
|
152
166
|
|
|
153
167
|
return session_id
|
|
154
168
|
|
|
169
|
+
@x_ipe_tracing()
|
|
155
170
|
def get_session(self, session_id: str) -> Optional[PersistentSession]:
|
|
156
171
|
"""Get session by ID."""
|
|
157
172
|
with self._lock:
|
|
158
173
|
return self.sessions.get(session_id)
|
|
159
174
|
|
|
175
|
+
@x_ipe_tracing()
|
|
160
176
|
def has_session(self, session_id: str) -> bool:
|
|
161
177
|
"""Check if session exists."""
|
|
162
178
|
with self._lock:
|
|
163
179
|
return session_id in self.sessions
|
|
164
180
|
|
|
181
|
+
@x_ipe_tracing()
|
|
165
182
|
def remove_session(self, session_id: str) -> None:
|
|
166
183
|
"""Remove and close a session."""
|
|
167
184
|
with self._lock:
|
|
@@ -169,6 +186,7 @@ class SessionManager:
|
|
|
169
186
|
if session:
|
|
170
187
|
session.close()
|
|
171
188
|
|
|
189
|
+
@x_ipe_tracing()
|
|
172
190
|
def cleanup_expired(self) -> int:
|
|
173
191
|
"""Remove expired sessions. Returns count removed."""
|
|
174
192
|
expired_ids = []
|
|
@@ -182,11 +200,13 @@ class SessionManager:
|
|
|
182
200
|
|
|
183
201
|
return len(expired_ids)
|
|
184
202
|
|
|
203
|
+
@x_ipe_tracing()
|
|
185
204
|
def start_cleanup_task(self) -> None:
|
|
186
205
|
"""Start background cleanup task (every 5 minutes)."""
|
|
187
206
|
self._running = True
|
|
188
207
|
self._schedule_cleanup()
|
|
189
208
|
|
|
209
|
+
@x_ipe_tracing()
|
|
190
210
|
def stop_cleanup_task(self) -> None:
|
|
191
211
|
"""Stop the cleanup task."""
|
|
192
212
|
self._running = False
|
|
@@ -230,6 +250,7 @@ class PTYSession:
|
|
|
230
250
|
self.rows = 24
|
|
231
251
|
self.cols = 80
|
|
232
252
|
|
|
253
|
+
@x_ipe_tracing()
|
|
233
254
|
def start(self, rows: int = 24, cols: int = 80) -> None:
|
|
234
255
|
"""Spawn PTY with shell and start output reader."""
|
|
235
256
|
import pty
|
|
@@ -310,6 +331,7 @@ class PTYSession:
|
|
|
310
331
|
except Exception:
|
|
311
332
|
pass
|
|
312
333
|
|
|
334
|
+
@x_ipe_tracing()
|
|
313
335
|
def write(self, data: str) -> None:
|
|
314
336
|
"""Write input to PTY."""
|
|
315
337
|
if self.fd is not None:
|
|
@@ -328,6 +350,7 @@ class PTYSession:
|
|
|
328
350
|
winsize = struct.pack('HHHH', self.rows, self.cols, 0, 0)
|
|
329
351
|
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
|
|
330
352
|
|
|
353
|
+
@x_ipe_tracing()
|
|
331
354
|
def close(self) -> None:
|
|
332
355
|
"""Terminate PTY session and cleanup."""
|
|
333
356
|
import signal
|
|
@@ -351,6 +374,7 @@ class PTYSession:
|
|
|
351
374
|
pass
|
|
352
375
|
self.pid = None
|
|
353
376
|
|
|
377
|
+
@x_ipe_tracing()
|
|
354
378
|
def isalive(self) -> bool:
|
|
355
379
|
"""Check if the PTY process is still running."""
|
|
356
380
|
return self._running and self.fd is not None
|
x_ipe/services/themes_service.py
CHANGED
|
@@ -10,6 +10,8 @@ import re
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Dict, List, Any, Optional
|
|
12
12
|
|
|
13
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
THEMES_DIR = 'x-ipe-docs/themes'
|
|
15
17
|
THEME_PREFIX = 'theme-'
|
|
@@ -45,6 +47,7 @@ class ThemesService:
|
|
|
45
47
|
self.project_root = Path(project_root).resolve()
|
|
46
48
|
self.themes_dir = self.project_root / THEMES_DIR
|
|
47
49
|
|
|
50
|
+
@x_ipe_tracing()
|
|
48
51
|
def list_themes(self) -> List[Dict[str, Any]]:
|
|
49
52
|
"""
|
|
50
53
|
List all valid themes with metadata.
|
|
@@ -92,6 +95,7 @@ class ThemesService:
|
|
|
92
95
|
|
|
93
96
|
return themes
|
|
94
97
|
|
|
98
|
+
@x_ipe_tracing()
|
|
95
99
|
def get_theme(self, name: str) -> Optional[Dict[str, Any]]:
|
|
96
100
|
"""
|
|
97
101
|
Get detailed information about a specific theme.
|
|
@@ -11,6 +11,8 @@ import copy
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, Any
|
|
13
13
|
|
|
14
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
CONFIG_DIR = 'x-ipe-docs/config'
|
|
16
18
|
CONFIG_FILE = 'tools.json'
|
|
@@ -56,6 +58,7 @@ class ToolsConfigService:
|
|
|
56
58
|
self.config_path = self.config_dir / CONFIG_FILE
|
|
57
59
|
self.legacy_path = self.project_root / LEGACY_PATH
|
|
58
60
|
|
|
61
|
+
@x_ipe_tracing()
|
|
59
62
|
def load(self) -> Dict[str, Any]:
|
|
60
63
|
"""
|
|
61
64
|
Load config, migrating from legacy if needed.
|
|
@@ -76,6 +79,7 @@ class ToolsConfigService:
|
|
|
76
79
|
|
|
77
80
|
return self._create_default()
|
|
78
81
|
|
|
82
|
+
@x_ipe_tracing()
|
|
79
83
|
def save(self, config: Dict[str, Any]) -> bool:
|
|
80
84
|
"""
|
|
81
85
|
Save config to file.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
TracingService for managing tracing configuration and lifecycle.
|
|
5
|
+
|
|
6
|
+
Provides high-level API for starting/stopping tracing, reading
|
|
7
|
+
configuration from tools.json, and cleaning up old log files.
|
|
8
|
+
"""
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Dict, List, Any, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from x_ipe.services.tools_config_service import ToolsConfigService
|
|
14
|
+
from x_ipe.tracing.writer import TraceLogWriter
|
|
15
|
+
from x_ipe.tracing.parser import TraceLogParser
|
|
16
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TracingService:
|
|
20
|
+
"""
|
|
21
|
+
Service for managing tracing configuration and lifecycle.
|
|
22
|
+
|
|
23
|
+
Integrates with tools.json for configuration persistence and
|
|
24
|
+
provides methods for controlling tracing state.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
service = TracingService("/path/to/project")
|
|
28
|
+
|
|
29
|
+
# Check status
|
|
30
|
+
config = service.get_config()
|
|
31
|
+
is_active = service.is_active()
|
|
32
|
+
|
|
33
|
+
# Control tracing
|
|
34
|
+
service.start(duration_minutes=15)
|
|
35
|
+
service.stop()
|
|
36
|
+
|
|
37
|
+
# List/cleanup logs
|
|
38
|
+
logs = service.list_logs()
|
|
39
|
+
deleted = service.cleanup_on_startup()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, project_root: str):
|
|
43
|
+
"""
|
|
44
|
+
Initialize TracingService.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
project_root: Path to the project root directory
|
|
48
|
+
"""
|
|
49
|
+
self.project_root = Path(project_root)
|
|
50
|
+
self.tools_config = ToolsConfigService(str(project_root))
|
|
51
|
+
|
|
52
|
+
@x_ipe_tracing()
|
|
53
|
+
def get_config(self) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Get current tracing configuration.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary with tracing settings:
|
|
59
|
+
- enabled: bool
|
|
60
|
+
- stop_at: str or None (ISO timestamp)
|
|
61
|
+
- log_path: str
|
|
62
|
+
- retention_hours: int
|
|
63
|
+
- ignored_apis: list
|
|
64
|
+
"""
|
|
65
|
+
config = self.tools_config.load()
|
|
66
|
+
return {
|
|
67
|
+
"enabled": config.get("tracing_enabled", False),
|
|
68
|
+
"stop_at": config.get("tracing_stop_at"),
|
|
69
|
+
"log_path": config.get("tracing_log_path", "instance/traces/"),
|
|
70
|
+
"retention_hours": config.get("tracing_retention_hours", 24),
|
|
71
|
+
"ignored_apis": config.get("tracing_ignored_apis", [])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@x_ipe_tracing()
|
|
75
|
+
def is_active(self) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Check if tracing is currently active.
|
|
78
|
+
|
|
79
|
+
Tracing is active if:
|
|
80
|
+
- tracing_enabled is True, OR
|
|
81
|
+
- tracing_stop_at is set and in the future
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if tracing should be performed
|
|
85
|
+
"""
|
|
86
|
+
config = self.get_config()
|
|
87
|
+
|
|
88
|
+
if config["enabled"]:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
stop_at = config["stop_at"]
|
|
92
|
+
if stop_at:
|
|
93
|
+
try:
|
|
94
|
+
# Parse ISO timestamp - keep it timezone-aware
|
|
95
|
+
stop_time = datetime.fromisoformat(
|
|
96
|
+
stop_at.replace("Z", "+00:00")
|
|
97
|
+
)
|
|
98
|
+
return datetime.now(timezone.utc) < stop_time
|
|
99
|
+
except (ValueError, AttributeError):
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
@x_ipe_tracing()
|
|
105
|
+
def start(self, duration_minutes: int) -> Dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Start tracing for specified duration.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
duration_minutes: Duration in minutes (must be 3, 15, or 30)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with success status and stop_at timestamp
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If duration is not 3, 15, or 30
|
|
117
|
+
"""
|
|
118
|
+
if duration_minutes not in [3, 15, 30]:
|
|
119
|
+
raise ValueError("Duration must be 3, 15, or 30 minutes")
|
|
120
|
+
|
|
121
|
+
stop_at = datetime.now(timezone.utc) + timedelta(minutes=duration_minutes)
|
|
122
|
+
# Convert +00:00 to Z for consistent format
|
|
123
|
+
stop_at_str = stop_at.isoformat().replace("+00:00", "Z")
|
|
124
|
+
|
|
125
|
+
config = self.tools_config.load()
|
|
126
|
+
config["tracing_stop_at"] = stop_at_str
|
|
127
|
+
self.tools_config.save(config)
|
|
128
|
+
|
|
129
|
+
return {"success": True, "stop_at": stop_at_str}
|
|
130
|
+
|
|
131
|
+
@x_ipe_tracing()
|
|
132
|
+
def stop(self) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Stop tracing immediately.
|
|
135
|
+
|
|
136
|
+
Clears tracing_stop_at and sets tracing_enabled to False.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dictionary with success status
|
|
140
|
+
"""
|
|
141
|
+
config = self.tools_config.load()
|
|
142
|
+
config["tracing_stop_at"] = None
|
|
143
|
+
config["tracing_enabled"] = False
|
|
144
|
+
self.tools_config.save(config)
|
|
145
|
+
|
|
146
|
+
return {"success": True}
|
|
147
|
+
|
|
148
|
+
@x_ipe_tracing()
|
|
149
|
+
def update_ignored_apis(self, patterns: List[str]) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Update the list of ignored API patterns.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
patterns: List of API path patterns to ignore
|
|
155
|
+
"""
|
|
156
|
+
config = self.tools_config.load()
|
|
157
|
+
config["tracing_ignored_apis"] = patterns
|
|
158
|
+
self.tools_config.save(config)
|
|
159
|
+
|
|
160
|
+
@x_ipe_tracing()
|
|
161
|
+
def list_logs(self) -> List[Dict[str, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
List all trace log files.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of log file metadata dictionaries:
|
|
167
|
+
- trace_id: str
|
|
168
|
+
- api: str (e.g., "GET /api/project/structure")
|
|
169
|
+
- filename: str
|
|
170
|
+
- size: int (bytes)
|
|
171
|
+
- timestamp: str (ISO format)
|
|
172
|
+
"""
|
|
173
|
+
config = self.get_config()
|
|
174
|
+
log_path = self.project_root / config["log_path"]
|
|
175
|
+
|
|
176
|
+
if not log_path.exists():
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
logs = []
|
|
180
|
+
for filepath in sorted(log_path.glob("*.log"), reverse=True):
|
|
181
|
+
try:
|
|
182
|
+
# Parse filename: {timestamp}-{api}-{trace_id}.log
|
|
183
|
+
# Example: 20260202-072505-get-api-project-structure-a649c048-3d73.log
|
|
184
|
+
stem = filepath.stem
|
|
185
|
+
|
|
186
|
+
# Extract trace_id (last 2 UUID segments: xxxxxxxx-xxxx)
|
|
187
|
+
# Split and find the UUID pattern at the end
|
|
188
|
+
parts = stem.split("-")
|
|
189
|
+
if len(parts) >= 4:
|
|
190
|
+
# Last 2 parts form the trace_id (e.g., "a649c048-3d73")
|
|
191
|
+
trace_id = f"{parts[-2]}-{parts[-1]}"
|
|
192
|
+
# First 2 parts are timestamp (YYYYMMDD-HHMMSS)
|
|
193
|
+
# Middle parts are the API name
|
|
194
|
+
api_parts = parts[2:-2] # Skip timestamp and trace_id
|
|
195
|
+
api_name = "-".join(api_parts) if api_parts else "unknown"
|
|
196
|
+
# Convert api_name back to path format (e.g., "get-api-project-structure" -> "GET /api/project/structure")
|
|
197
|
+
api = self._filename_to_api(api_name)
|
|
198
|
+
else:
|
|
199
|
+
trace_id = stem
|
|
200
|
+
api = "/unknown"
|
|
201
|
+
|
|
202
|
+
stat = filepath.stat()
|
|
203
|
+
logs.append({
|
|
204
|
+
"trace_id": trace_id,
|
|
205
|
+
"api": api,
|
|
206
|
+
"filename": filepath.name,
|
|
207
|
+
"size": stat.st_size,
|
|
208
|
+
"timestamp": datetime.fromtimestamp(stat.st_mtime).isoformat()
|
|
209
|
+
})
|
|
210
|
+
except OSError:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
return logs
|
|
214
|
+
|
|
215
|
+
def _filename_to_api(self, api_name: str) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Convert sanitized API filename component back to API format.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
api_name: Sanitized name (e.g., "get-api-project-structure")
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
API string (e.g., "GET /api/project/structure")
|
|
224
|
+
"""
|
|
225
|
+
if not api_name or api_name == "unknown":
|
|
226
|
+
return "/unknown"
|
|
227
|
+
|
|
228
|
+
# Split by first hyphen to get method
|
|
229
|
+
parts = api_name.split("-", 1)
|
|
230
|
+
if len(parts) < 2:
|
|
231
|
+
return f"/{api_name}"
|
|
232
|
+
|
|
233
|
+
method = parts[0].upper()
|
|
234
|
+
path_part = parts[1]
|
|
235
|
+
|
|
236
|
+
# Convert hyphens back to slashes for path
|
|
237
|
+
path = "/" + path_part.replace("-", "/")
|
|
238
|
+
|
|
239
|
+
return f"{method} {path}"
|
|
240
|
+
|
|
241
|
+
@x_ipe_tracing()
|
|
242
|
+
def cleanup_on_startup(self) -> int:
|
|
243
|
+
"""
|
|
244
|
+
Clean up old log files on backend startup.
|
|
245
|
+
|
|
246
|
+
Uses retention_hours from configuration to determine
|
|
247
|
+
which files to delete.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Number of files deleted
|
|
251
|
+
"""
|
|
252
|
+
config = self.get_config()
|
|
253
|
+
log_path = self.project_root / config["log_path"]
|
|
254
|
+
|
|
255
|
+
writer = TraceLogWriter(str(log_path))
|
|
256
|
+
deleted = writer.cleanup(config["retention_hours"])
|
|
257
|
+
|
|
258
|
+
if deleted > 0:
|
|
259
|
+
print(f"[TRACING] Cleaned up {deleted} old trace log(s)")
|
|
260
|
+
|
|
261
|
+
return deleted
|
|
262
|
+
|
|
263
|
+
@x_ipe_tracing()
|
|
264
|
+
def delete_all_logs(self) -> int:
|
|
265
|
+
"""
|
|
266
|
+
Delete all trace log files.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Number of files deleted
|
|
270
|
+
"""
|
|
271
|
+
config = self.get_config()
|
|
272
|
+
log_path = self.project_root / config["log_path"]
|
|
273
|
+
|
|
274
|
+
if not log_path.exists():
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
deleted = 0
|
|
278
|
+
for filepath in log_path.glob("*.log"):
|
|
279
|
+
try:
|
|
280
|
+
filepath.unlink()
|
|
281
|
+
deleted += 1
|
|
282
|
+
except OSError:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
return deleted
|
|
286
|
+
|
|
287
|
+
@x_ipe_tracing()
|
|
288
|
+
def get_trace(self, trace_id: str) -> Optional[Dict[str, Any]]:
|
|
289
|
+
"""
|
|
290
|
+
Get parsed trace data for visualization.
|
|
291
|
+
|
|
292
|
+
Searches for a log file matching the trace_id (exact or partial)
|
|
293
|
+
and parses it into visualization-ready structure.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
trace_id: Full or partial trace ID to search for
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Parsed trace data or None if not found:
|
|
300
|
+
{
|
|
301
|
+
"trace_id": str,
|
|
302
|
+
"api": str,
|
|
303
|
+
"timestamp": str,
|
|
304
|
+
"total_time_ms": int,
|
|
305
|
+
"status": str,
|
|
306
|
+
"nodes": [...],
|
|
307
|
+
"edges": [...]
|
|
308
|
+
}
|
|
309
|
+
"""
|
|
310
|
+
config = self.get_config()
|
|
311
|
+
log_path = self.project_root / config["log_path"]
|
|
312
|
+
|
|
313
|
+
if not log_path.exists():
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
# Search for matching file
|
|
317
|
+
matching_file = None
|
|
318
|
+
for filepath in log_path.glob("*.log"):
|
|
319
|
+
if trace_id in filepath.stem:
|
|
320
|
+
matching_file = filepath
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if not matching_file:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
# Parse the file
|
|
327
|
+
parser = TraceLogParser()
|
|
328
|
+
result = parser.parse(matching_file)
|
|
329
|
+
|
|
330
|
+
# Add filename for reference
|
|
331
|
+
result["filename"] = matching_file.name
|
|
332
|
+
|
|
333
|
+
return result
|
|
@@ -4,8 +4,12 @@ FEATURE-022-D: UI/UX Feedback Service
|
|
|
4
4
|
Handles saving feedback entries to the file system.
|
|
5
5
|
"""
|
|
6
6
|
import base64
|
|
7
|
+
import shutil
|
|
8
|
+
import re
|
|
7
9
|
from pathlib import Path
|
|
8
|
-
from datetime import datetime
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
|
|
12
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
class UiuxFeedbackService:
|
|
@@ -15,6 +19,149 @@ class UiuxFeedbackService:
|
|
|
15
19
|
self.project_root = Path(project_root)
|
|
16
20
|
self.feedback_dir = self.project_root / 'x-ipe-docs' / 'uiux-feedback'
|
|
17
21
|
|
|
22
|
+
@x_ipe_tracing()
|
|
23
|
+
def list_feedback(self, days: int = 2) -> list:
|
|
24
|
+
"""
|
|
25
|
+
List feedback entries from the last N days.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
days: Number of days to look back (default 2)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of feedback entries sorted by date descending
|
|
32
|
+
"""
|
|
33
|
+
entries = []
|
|
34
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
35
|
+
|
|
36
|
+
if not self.feedback_dir.exists():
|
|
37
|
+
return entries
|
|
38
|
+
|
|
39
|
+
for folder in self.feedback_dir.iterdir():
|
|
40
|
+
if not folder.is_dir():
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# Check folder modification time
|
|
44
|
+
mtime = datetime.fromtimestamp(folder.stat().st_mtime)
|
|
45
|
+
if mtime < cutoff:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Parse feedback.md to extract details
|
|
49
|
+
feedback_md = folder / 'feedback.md'
|
|
50
|
+
if not feedback_md.exists():
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
entry = self._parse_feedback_md(folder.name, feedback_md)
|
|
54
|
+
if entry:
|
|
55
|
+
entry['mtime'] = mtime
|
|
56
|
+
entries.append(entry)
|
|
57
|
+
|
|
58
|
+
# Sort by date descending (newest first)
|
|
59
|
+
entries.sort(key=lambda x: x['mtime'], reverse=True)
|
|
60
|
+
|
|
61
|
+
# Remove mtime from output (internal use only)
|
|
62
|
+
for entry in entries:
|
|
63
|
+
del entry['mtime']
|
|
64
|
+
|
|
65
|
+
return entries
|
|
66
|
+
|
|
67
|
+
def _parse_feedback_md(self, folder_name: str, feedback_md: Path) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Parse feedback.md file to extract entry details.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
folder_name: Name of the feedback folder
|
|
73
|
+
feedback_md: Path to feedback.md file
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with id, name, url, description, date
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
content = feedback_md.read_text(encoding='utf-8')
|
|
80
|
+
|
|
81
|
+
# Extract URL
|
|
82
|
+
url_match = re.search(r'\*\*URL:\*\*\s*(.+)', content)
|
|
83
|
+
url = url_match.group(1).strip() if url_match else ''
|
|
84
|
+
|
|
85
|
+
# Extract date
|
|
86
|
+
date_match = re.search(r'\*\*Date:\*\*\s*(.+)', content)
|
|
87
|
+
date = date_match.group(1).strip() if date_match else ''
|
|
88
|
+
|
|
89
|
+
# Extract description (between ## Feedback and ## Screenshot or end)
|
|
90
|
+
desc_match = re.search(r'## Feedback\s*\n\n(.+?)(?=\n## Screenshot|\Z)', content, re.DOTALL)
|
|
91
|
+
description = desc_match.group(1).strip() if desc_match else ''
|
|
92
|
+
if description == '_No description provided_':
|
|
93
|
+
description = ''
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
'id': folder_name,
|
|
97
|
+
'name': folder_name,
|
|
98
|
+
'url': url,
|
|
99
|
+
'description': description,
|
|
100
|
+
'date': date
|
|
101
|
+
}
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
@x_ipe_tracing()
|
|
106
|
+
def delete_feedback(self, feedback_id: str) -> dict:
|
|
107
|
+
"""
|
|
108
|
+
Delete a feedback folder by ID.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
feedback_id: The feedback folder name/ID
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
dict with success (or error on failure)
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
folder_path = self.feedback_dir / feedback_id
|
|
118
|
+
|
|
119
|
+
if not folder_path.exists():
|
|
120
|
+
return {'success': False, 'error': 'Feedback not found'}
|
|
121
|
+
|
|
122
|
+
if not folder_path.is_dir():
|
|
123
|
+
return {'success': False, 'error': 'Invalid feedback ID'}
|
|
124
|
+
|
|
125
|
+
# Delete the folder
|
|
126
|
+
shutil.rmtree(folder_path)
|
|
127
|
+
|
|
128
|
+
return {'success': True}
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return {'success': False, 'error': str(e)}
|
|
131
|
+
|
|
132
|
+
@x_ipe_tracing()
|
|
133
|
+
def cleanup_old_feedback(self, days: int = 7) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Delete feedback folders older than N days.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
days: Retention period in days (default 7)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Number of folders deleted
|
|
142
|
+
"""
|
|
143
|
+
deleted = 0
|
|
144
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
145
|
+
|
|
146
|
+
if not self.feedback_dir.exists():
|
|
147
|
+
return deleted
|
|
148
|
+
|
|
149
|
+
for folder in list(self.feedback_dir.iterdir()):
|
|
150
|
+
if not folder.is_dir():
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Check folder modification time
|
|
154
|
+
mtime = datetime.fromtimestamp(folder.stat().st_mtime)
|
|
155
|
+
if mtime < cutoff:
|
|
156
|
+
try:
|
|
157
|
+
shutil.rmtree(folder)
|
|
158
|
+
deleted += 1
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
return deleted
|
|
163
|
+
|
|
164
|
+
@x_ipe_tracing()
|
|
18
165
|
def save_feedback(self, data: dict) -> dict:
|
|
19
166
|
"""
|
|
20
167
|
Save feedback entry to file system.
|