x-ipe 1.0.24__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 +25 -3
- 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 +17 -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 +2 -0
- 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 +30 -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 +19 -0
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +4 -0
- 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 +32 -0
- 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 +1 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/workplace.css +20 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -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-search.js +6 -2
- x_ipe/static/js/features/workplace.js +200 -6
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/uiux-feedback.js +18 -2
- x_ipe/templates/base.html +19 -0
- x_ipe/templates/index.html +7 -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.24.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/RECORD +132 -62
- 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.24.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Middleware
|
|
3
|
+
|
|
4
|
+
Flask middleware for automatic request tracing.
|
|
5
|
+
|
|
6
|
+
Creates TraceContext for each request when tracing is active,
|
|
7
|
+
then writes the trace log file after the request completes.
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from flask import Flask, request, g
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from .context import TraceContext
|
|
16
|
+
from .writer import TraceLogWriter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# APIs to ignore (to avoid infinite loops and noise)
|
|
20
|
+
IGNORED_API_PREFIXES = (
|
|
21
|
+
'/api/tracing/', # Tracing APIs themselves
|
|
22
|
+
'/static/', # Static files
|
|
23
|
+
'/socket.io/', # WebSocket
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def init_tracing_middleware(app: Flask) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize tracing middleware for a Flask app.
|
|
30
|
+
|
|
31
|
+
Registers before_request and after_request hooks that:
|
|
32
|
+
- Check if tracing is active
|
|
33
|
+
- Start TraceContext before each request
|
|
34
|
+
- End TraceContext and write log file after each request
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
app: Flask application instance
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@app.before_request
|
|
41
|
+
def start_trace():
|
|
42
|
+
"""Start trace context if tracing is active."""
|
|
43
|
+
# Skip hardcoded ignored paths (system paths)
|
|
44
|
+
if request.path.startswith(IGNORED_API_PREFIXES):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Check if tracing is active and get config
|
|
48
|
+
project_root = app.config.get('PROJECT_ROOT', '.')
|
|
49
|
+
active, ignored_apis = _get_tracing_config(project_root)
|
|
50
|
+
|
|
51
|
+
if not active:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Skip user-configured ignored APIs
|
|
55
|
+
if _is_path_ignored(request.path, ignored_apis):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Start trace context
|
|
59
|
+
api_name = f"{request.method} {request.path}"
|
|
60
|
+
TraceContext.start_trace(api_name)
|
|
61
|
+
g.trace_start_time = time.perf_counter()
|
|
62
|
+
|
|
63
|
+
@app.after_request
|
|
64
|
+
def end_trace(response):
|
|
65
|
+
"""End trace context and write log file."""
|
|
66
|
+
# Check if we started a trace
|
|
67
|
+
if not hasattr(g, 'trace_start_time'):
|
|
68
|
+
return response
|
|
69
|
+
|
|
70
|
+
# End the trace
|
|
71
|
+
buffer = TraceContext.end_trace()
|
|
72
|
+
if buffer is None:
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
# Determine status from response
|
|
76
|
+
status = "SUCCESS" if response.status_code < 400 else "ERROR"
|
|
77
|
+
|
|
78
|
+
# Get log path from config
|
|
79
|
+
project_root = app.config.get('PROJECT_ROOT', '.')
|
|
80
|
+
log_path = _get_trace_log_path(project_root)
|
|
81
|
+
|
|
82
|
+
# Write trace to file
|
|
83
|
+
writer = TraceLogWriter(log_path)
|
|
84
|
+
writer.write(buffer, status)
|
|
85
|
+
|
|
86
|
+
return response
|
|
87
|
+
|
|
88
|
+
@app.teardown_request
|
|
89
|
+
def cleanup_trace(exception=None):
|
|
90
|
+
"""Clean up trace context on error."""
|
|
91
|
+
if exception is not None:
|
|
92
|
+
buffer = TraceContext.end_trace()
|
|
93
|
+
if buffer is not None:
|
|
94
|
+
project_root = app.config.get('PROJECT_ROOT', '.')
|
|
95
|
+
log_path = _get_trace_log_path(project_root)
|
|
96
|
+
writer = TraceLogWriter(log_path)
|
|
97
|
+
writer.write(buffer, "ERROR")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_tracing_config(project_root: str) -> tuple:
|
|
101
|
+
"""
|
|
102
|
+
Get tracing configuration including active status and ignored APIs.
|
|
103
|
+
|
|
104
|
+
Reads from tools.json to check tracing_enabled, tracing_stop_at,
|
|
105
|
+
and tracing_ignored_apis.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
project_root: Path to project root
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Tuple of (is_active: bool, ignored_apis: List[str])
|
|
112
|
+
"""
|
|
113
|
+
import json
|
|
114
|
+
|
|
115
|
+
tools_path = Path(project_root) / "x-ipe-docs" / "config" / "tools.json"
|
|
116
|
+
if not tools_path.exists():
|
|
117
|
+
return False, []
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
with open(tools_path) as f:
|
|
121
|
+
config = json.load(f)
|
|
122
|
+
except (json.JSONDecodeError, IOError):
|
|
123
|
+
return False, []
|
|
124
|
+
|
|
125
|
+
ignored_apis = config.get("tracing_ignored_apis", [])
|
|
126
|
+
|
|
127
|
+
# Check if explicitly enabled
|
|
128
|
+
if config.get("tracing_enabled", False):
|
|
129
|
+
return True, ignored_apis
|
|
130
|
+
|
|
131
|
+
# Check if stop_at is in the future
|
|
132
|
+
stop_at = config.get("tracing_stop_at")
|
|
133
|
+
if stop_at:
|
|
134
|
+
try:
|
|
135
|
+
# Keep timezone-aware for proper comparison
|
|
136
|
+
stop_time = datetime.fromisoformat(
|
|
137
|
+
stop_at.replace("Z", "+00:00")
|
|
138
|
+
)
|
|
139
|
+
if datetime.now(timezone.utc) < stop_time:
|
|
140
|
+
return True, ignored_apis
|
|
141
|
+
except (ValueError, AttributeError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
return False, ignored_apis
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_path_ignored(path: str, ignored_apis: List[str]) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Check if a request path matches any user-configured ignored API patterns.
|
|
150
|
+
|
|
151
|
+
Supports exact matches and prefix matches (patterns ending with *).
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
path: Request path (e.g., "/api/project/structure")
|
|
155
|
+
ignored_apis: List of API patterns to ignore
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if path should be ignored
|
|
159
|
+
"""
|
|
160
|
+
for pattern in ignored_apis:
|
|
161
|
+
if pattern.endswith('*'):
|
|
162
|
+
# Prefix match
|
|
163
|
+
if path.startswith(pattern[:-1]):
|
|
164
|
+
return True
|
|
165
|
+
else:
|
|
166
|
+
# Exact match
|
|
167
|
+
if path == pattern:
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_trace_log_path(project_root: str) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Get the trace log directory path.
|
|
175
|
+
|
|
176
|
+
Reads from tools.json or uses default.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
project_root: Path to project root
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Absolute path to trace log directory
|
|
183
|
+
"""
|
|
184
|
+
import json
|
|
185
|
+
|
|
186
|
+
tools_path = Path(project_root) / "x-ipe-docs" / "config" / "tools.json"
|
|
187
|
+
log_path = "instance/traces/"
|
|
188
|
+
|
|
189
|
+
if tools_path.exists():
|
|
190
|
+
try:
|
|
191
|
+
with open(tools_path) as f:
|
|
192
|
+
config = json.load(f)
|
|
193
|
+
log_path = config.get("tracing_log_path", log_path)
|
|
194
|
+
except (json.JSONDecodeError, IOError):
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
return str(Path(project_root) / log_path)
|
x_ipe/tracing/parser.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023-C: Trace Viewer & DAG Visualization
|
|
3
|
+
|
|
4
|
+
TraceLogParser for parsing trace log files into visualization-ready graph structures.
|
|
5
|
+
|
|
6
|
+
Handles the log format:
|
|
7
|
+
[TRACE-START] trace_id | API | timestamp
|
|
8
|
+
[INFO] → start_function: name | input_json
|
|
9
|
+
[INFO] ← return_function: name | output_json | duration
|
|
10
|
+
[ERROR] ← exception: name | error | duration
|
|
11
|
+
[TRACE-END] trace_id | total_duration | status
|
|
12
|
+
"""
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Any, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TraceLogParser:
|
|
19
|
+
"""
|
|
20
|
+
Parse trace log files into graph structure for visualization.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
parser = TraceLogParser()
|
|
24
|
+
result = parser.parse(Path("trace.log"))
|
|
25
|
+
# result = {"trace_id": "...", "nodes": [...], "edges": [...]}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Regex patterns for parsing log lines
|
|
29
|
+
TRACE_START_PATTERN = re.compile(
|
|
30
|
+
r'\[TRACE-START\]\s*([^\|]+)\s*\|\s*([^\|]+)\s*\|\s*(.+)'
|
|
31
|
+
)
|
|
32
|
+
TRACE_END_PATTERN = re.compile(
|
|
33
|
+
r'\[TRACE-END\]\s*([^\|]+)\s*\|\s*(\d+)ms\s*\|\s*(\w+)'
|
|
34
|
+
)
|
|
35
|
+
FUNCTION_START_PATTERN = re.compile(
|
|
36
|
+
r'\[(INFO|DEBUG)\]\s*→\s*start_function:\s*([^\|]+)\s*\|\s*(.+)'
|
|
37
|
+
)
|
|
38
|
+
FUNCTION_RETURN_PATTERN = re.compile(
|
|
39
|
+
r'\[(INFO|DEBUG)\]\s*←\s*return_function:\s*([^\|]+)\s*\|\s*([^\|]+)\s*\|\s*(\d+)ms'
|
|
40
|
+
)
|
|
41
|
+
EXCEPTION_PATTERN = re.compile(
|
|
42
|
+
r'\[(ERROR|INFO|DEBUG)\]\s*←\s*exception:\s*([^\|]+)\s*\|\s*([^|]+):\s*([^\|]+)\s*\|\s*(\d+)ms'
|
|
43
|
+
)
|
|
44
|
+
STACK_LINE_PATTERN = re.compile(
|
|
45
|
+
r'\s+at\s+(\w+)\s+\(([^:]+):?(\d+)?\)'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def parse(self, filepath: Path) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Parse trace log file into visualization-ready structure.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
filepath: Path to the trace log file
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary with trace data:
|
|
57
|
+
{
|
|
58
|
+
"trace_id": str,
|
|
59
|
+
"api": str,
|
|
60
|
+
"timestamp": str,
|
|
61
|
+
"total_time_ms": int,
|
|
62
|
+
"status": str ("success" or "error"),
|
|
63
|
+
"nodes": [
|
|
64
|
+
{
|
|
65
|
+
"id": str,
|
|
66
|
+
"label": str,
|
|
67
|
+
"timing": str,
|
|
68
|
+
"status": str,
|
|
69
|
+
"level": str,
|
|
70
|
+
"input": str,
|
|
71
|
+
"output": str,
|
|
72
|
+
"error": dict or None
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"edges": [
|
|
76
|
+
{"source": str, "target": str}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
"""
|
|
80
|
+
result = {
|
|
81
|
+
"trace_id": "",
|
|
82
|
+
"api": "",
|
|
83
|
+
"timestamp": "",
|
|
84
|
+
"total_time_ms": 0,
|
|
85
|
+
"status": "success",
|
|
86
|
+
"nodes": [],
|
|
87
|
+
"edges": []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if not filepath.exists():
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
content = filepath.read_text()
|
|
94
|
+
if not content.strip():
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
lines = content.splitlines()
|
|
98
|
+
nodes = []
|
|
99
|
+
edges = []
|
|
100
|
+
call_stack = [] # Stack of node indices for tracking parent-child
|
|
101
|
+
node_counter = 0
|
|
102
|
+
current_error_node = None
|
|
103
|
+
stack_lines = []
|
|
104
|
+
|
|
105
|
+
for raw_line in lines:
|
|
106
|
+
# Keep original line for stack trace matching
|
|
107
|
+
line = raw_line.strip()
|
|
108
|
+
|
|
109
|
+
# Check for stack trace lines (indented with 'at')
|
|
110
|
+
stack_match = self.STACK_LINE_PATTERN.match(raw_line)
|
|
111
|
+
if stack_match and current_error_node is not None:
|
|
112
|
+
func, file_path, line_num = stack_match.groups()
|
|
113
|
+
stack_lines.append({
|
|
114
|
+
"func": func,
|
|
115
|
+
"file": file_path,
|
|
116
|
+
"line": int(line_num) if line_num else None
|
|
117
|
+
})
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# If we were collecting stack and hit non-stack line, finalize
|
|
121
|
+
if stack_lines and current_error_node is not None:
|
|
122
|
+
nodes[current_error_node]["error"]["stack"] = stack_lines
|
|
123
|
+
stack_lines = []
|
|
124
|
+
current_error_node = None
|
|
125
|
+
|
|
126
|
+
# TRACE-START
|
|
127
|
+
start_match = self.TRACE_START_PATTERN.match(line)
|
|
128
|
+
if start_match:
|
|
129
|
+
trace_id, api, timestamp = start_match.groups()
|
|
130
|
+
result["trace_id"] = trace_id.strip()
|
|
131
|
+
result["api"] = api.strip()
|
|
132
|
+
result["timestamp"] = timestamp.strip()
|
|
133
|
+
|
|
134
|
+
# Create root API node
|
|
135
|
+
nodes.append({
|
|
136
|
+
"id": f"node-{node_counter}",
|
|
137
|
+
"label": api.strip(),
|
|
138
|
+
"timing": "",
|
|
139
|
+
"status": "success",
|
|
140
|
+
"level": "API",
|
|
141
|
+
"input": "{}",
|
|
142
|
+
"output": "{}",
|
|
143
|
+
"error": None
|
|
144
|
+
})
|
|
145
|
+
call_stack.append(node_counter)
|
|
146
|
+
node_counter += 1
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# TRACE-END
|
|
150
|
+
end_match = self.TRACE_END_PATTERN.match(line)
|
|
151
|
+
if end_match:
|
|
152
|
+
_, total_ms, status = end_match.groups()
|
|
153
|
+
result["total_time_ms"] = int(total_ms)
|
|
154
|
+
result["status"] = status.lower()
|
|
155
|
+
|
|
156
|
+
# Update root node timing
|
|
157
|
+
if nodes:
|
|
158
|
+
nodes[0]["timing"] = f"{total_ms}ms"
|
|
159
|
+
if status.upper() == "ERROR":
|
|
160
|
+
nodes[0]["status"] = "error"
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Function start
|
|
164
|
+
func_start_match = self.FUNCTION_START_PATTERN.match(line)
|
|
165
|
+
if func_start_match:
|
|
166
|
+
level, func_name, input_json = func_start_match.groups()
|
|
167
|
+
func_name = func_name.strip()
|
|
168
|
+
|
|
169
|
+
# Create function node
|
|
170
|
+
new_node = {
|
|
171
|
+
"id": f"node-{node_counter}",
|
|
172
|
+
"label": func_name,
|
|
173
|
+
"timing": "",
|
|
174
|
+
"status": "success",
|
|
175
|
+
"level": level,
|
|
176
|
+
"input": input_json.strip(),
|
|
177
|
+
"output": "{}",
|
|
178
|
+
"error": None
|
|
179
|
+
}
|
|
180
|
+
nodes.append(new_node)
|
|
181
|
+
|
|
182
|
+
# Create edge from parent
|
|
183
|
+
if call_stack:
|
|
184
|
+
parent_id = call_stack[-1]
|
|
185
|
+
edges.append({
|
|
186
|
+
"source": f"node-{parent_id}",
|
|
187
|
+
"target": f"node-{node_counter}"
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
call_stack.append(node_counter)
|
|
191
|
+
node_counter += 1
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# Function return
|
|
195
|
+
func_return_match = self.FUNCTION_RETURN_PATTERN.match(line)
|
|
196
|
+
if func_return_match:
|
|
197
|
+
level, func_name, output_json, duration = func_return_match.groups()
|
|
198
|
+
func_name = func_name.strip()
|
|
199
|
+
|
|
200
|
+
# Pop from stack and update node
|
|
201
|
+
if call_stack:
|
|
202
|
+
current_id = call_stack.pop()
|
|
203
|
+
if current_id < len(nodes):
|
|
204
|
+
nodes[current_id]["output"] = output_json.strip()
|
|
205
|
+
nodes[current_id]["timing"] = f"{duration}ms"
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Exception
|
|
209
|
+
exception_match = self.EXCEPTION_PATTERN.match(line)
|
|
210
|
+
if exception_match:
|
|
211
|
+
level, func_name, error_type, error_msg, duration = exception_match.groups()
|
|
212
|
+
func_name = func_name.strip()
|
|
213
|
+
|
|
214
|
+
# Pop from stack and update node with error
|
|
215
|
+
if call_stack:
|
|
216
|
+
current_id = call_stack.pop()
|
|
217
|
+
if current_id < len(nodes):
|
|
218
|
+
nodes[current_id]["status"] = "error"
|
|
219
|
+
nodes[current_id]["timing"] = f"{duration}ms"
|
|
220
|
+
nodes[current_id]["error"] = {
|
|
221
|
+
"type": error_type.strip(),
|
|
222
|
+
"message": error_msg.strip(),
|
|
223
|
+
"stack": []
|
|
224
|
+
}
|
|
225
|
+
current_error_node = current_id
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# Finalize any remaining stack lines
|
|
229
|
+
if stack_lines and current_error_node is not None:
|
|
230
|
+
nodes[current_error_node]["error"]["stack"] = stack_lines
|
|
231
|
+
|
|
232
|
+
result["nodes"] = nodes
|
|
233
|
+
result["edges"] = edges
|
|
234
|
+
|
|
235
|
+
return result
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
Redactor module for sensitive data redaction in trace logs.
|
|
5
|
+
|
|
6
|
+
This module provides automatic redaction of sensitive data before logging,
|
|
7
|
+
including passwords, tokens, API keys, and credit card numbers.
|
|
8
|
+
"""
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Set, Optional
|
|
11
|
+
|
|
12
|
+
REDACTED = "[REDACTED]"
|
|
13
|
+
|
|
14
|
+
# Sensitive key patterns (case-insensitive matching)
|
|
15
|
+
SENSITIVE_KEY_PATTERNS = {
|
|
16
|
+
"password",
|
|
17
|
+
"secret",
|
|
18
|
+
"token",
|
|
19
|
+
"api_key",
|
|
20
|
+
"apikey",
|
|
21
|
+
"authorization",
|
|
22
|
+
"auth",
|
|
23
|
+
"credential",
|
|
24
|
+
"private_key",
|
|
25
|
+
"privatekey",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Value patterns
|
|
29
|
+
CREDIT_CARD_PATTERN = re.compile(r"^\d{16}$")
|
|
30
|
+
JWT_PREFIX = "eyJ"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Redactor:
|
|
34
|
+
"""
|
|
35
|
+
Sensitive data redactor for trace logs.
|
|
36
|
+
|
|
37
|
+
Automatically redacts:
|
|
38
|
+
- Fields containing sensitive key patterns (password, secret, token, etc.)
|
|
39
|
+
- Credit card numbers (16-digit patterns)
|
|
40
|
+
- JWT tokens (values starting with 'eyJ')
|
|
41
|
+
- Custom fields specified via constructor
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
redactor = Redactor(custom_fields=["ssn", "dob"])
|
|
45
|
+
safe_data = redactor.redact({"password": "secret", "name": "John"})
|
|
46
|
+
# Result: {"password": "[REDACTED]", "name": "John"}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, custom_fields: Optional[List[str]] = None):
|
|
50
|
+
"""
|
|
51
|
+
Initialize Redactor with optional custom fields.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
custom_fields: Additional field names to redact (case-insensitive)
|
|
55
|
+
"""
|
|
56
|
+
self.custom_fields: Set[str] = set(
|
|
57
|
+
f.lower() for f in (custom_fields or [])
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def redact(self, data: Any) -> Any:
|
|
61
|
+
"""
|
|
62
|
+
Recursively redact sensitive data.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: Data to redact (dict, list, or primitive)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Data with sensitive values replaced with '[REDACTED]'
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
return {k: self._redact_value(k, v) for k, v in data.items()}
|
|
72
|
+
elif isinstance(data, list):
|
|
73
|
+
return [self.redact(item) for item in data]
|
|
74
|
+
elif isinstance(data, tuple):
|
|
75
|
+
return tuple(self.redact(item) for item in data)
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
def _redact_value(self, key: str, value: Any) -> Any:
|
|
79
|
+
"""
|
|
80
|
+
Redact a single value based on key name and value patterns.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
key: Field name
|
|
84
|
+
value: Field value
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Redacted value or original value if not sensitive
|
|
88
|
+
"""
|
|
89
|
+
key_lower = key.lower()
|
|
90
|
+
|
|
91
|
+
# Check field name against sensitive patterns
|
|
92
|
+
for pattern in SENSITIVE_KEY_PATTERNS:
|
|
93
|
+
if pattern in key_lower:
|
|
94
|
+
return REDACTED
|
|
95
|
+
|
|
96
|
+
# Check custom fields
|
|
97
|
+
if key_lower in self.custom_fields:
|
|
98
|
+
return REDACTED
|
|
99
|
+
|
|
100
|
+
# Check value patterns for strings
|
|
101
|
+
if isinstance(value, str):
|
|
102
|
+
# Credit card pattern (16 digits)
|
|
103
|
+
if CREDIT_CARD_PATTERN.match(value):
|
|
104
|
+
return REDACTED
|
|
105
|
+
|
|
106
|
+
# JWT pattern (starts with eyJ)
|
|
107
|
+
if value.startswith(JWT_PREFIX):
|
|
108
|
+
return REDACTED
|
|
109
|
+
|
|
110
|
+
# Recurse for nested structures
|
|
111
|
+
return self.redact(value)
|
x_ipe/tracing/writer.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
TraceLogWriter for writing trace buffers to log files.
|
|
5
|
+
|
|
6
|
+
Handles file naming, directory creation, permission setting,
|
|
7
|
+
and cleanup of old log files.
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .buffer import TraceBuffer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TraceLogWriter:
|
|
19
|
+
"""
|
|
20
|
+
Writer for trace log files.
|
|
21
|
+
|
|
22
|
+
Writes trace buffers to structured log files with proper naming,
|
|
23
|
+
permissions, and cleanup of old files.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
writer = TraceLogWriter("instance/traces/")
|
|
27
|
+
filepath = writer.write(buffer, "SUCCESS")
|
|
28
|
+
deleted = writer.cleanup(retention_hours=24)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, log_path: str = "instance/traces/"):
|
|
32
|
+
"""
|
|
33
|
+
Initialize TraceLogWriter.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
log_path: Directory path for log files
|
|
37
|
+
"""
|
|
38
|
+
self.log_path = Path(log_path)
|
|
39
|
+
|
|
40
|
+
def write(self, buffer: TraceBuffer, status: str = "SUCCESS") -> Optional[str]:
|
|
41
|
+
"""
|
|
42
|
+
Write trace buffer to log file.
|
|
43
|
+
|
|
44
|
+
Creates the log directory if it doesn't exist.
|
|
45
|
+
Sets file permissions to 600 (owner read/write only).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
buffer: TraceBuffer to write
|
|
49
|
+
status: Final status (SUCCESS, ERROR)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Path to created log file, or None if write failed
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
# Ensure directory exists
|
|
56
|
+
self.log_path.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Calculate total duration
|
|
59
|
+
total_ms = (datetime.now(timezone.utc) - buffer.started_at).total_seconds() * 1000
|
|
60
|
+
content = buffer.to_log_string(status, total_ms)
|
|
61
|
+
|
|
62
|
+
# Generate filename
|
|
63
|
+
timestamp = buffer.started_at.strftime("%Y%m%d-%H%M%S")
|
|
64
|
+
api_name = self._sanitize_api_name(buffer.root_api)
|
|
65
|
+
filename = f"{timestamp}-{api_name}-{buffer.trace_id}.log"
|
|
66
|
+
filepath = self.log_path / filename
|
|
67
|
+
|
|
68
|
+
# Write file
|
|
69
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
70
|
+
f.write(content)
|
|
71
|
+
|
|
72
|
+
# Set permissions (owner read/write only)
|
|
73
|
+
os.chmod(filepath, 0o600)
|
|
74
|
+
|
|
75
|
+
return str(filepath)
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"[TRACING] Failed to write log: {e}")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def cleanup(self, retention_hours: int = 24) -> int:
|
|
82
|
+
"""
|
|
83
|
+
Delete log files older than retention period.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
retention_hours: Hours to retain log files
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Number of files deleted
|
|
90
|
+
"""
|
|
91
|
+
if not self.log_path.exists():
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
deleted = 0
|
|
95
|
+
cutoff = datetime.now(timezone.utc).timestamp() - (retention_hours * 3600)
|
|
96
|
+
|
|
97
|
+
for filepath in self.log_path.glob("*.log"):
|
|
98
|
+
try:
|
|
99
|
+
if filepath.stat().st_mtime < cutoff:
|
|
100
|
+
filepath.unlink()
|
|
101
|
+
deleted += 1
|
|
102
|
+
except OSError:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
return deleted
|
|
106
|
+
|
|
107
|
+
def _sanitize_api_name(self, api: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Convert API string to safe filename component.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
api: API string (e.g., "POST /api/orders")
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Sanitized string (e.g., "post--api-orders")
|
|
116
|
+
"""
|
|
117
|
+
# Replace spaces and slashes with hyphens
|
|
118
|
+
sanitized = api.lower().replace(" ", "-").replace("/", "-")
|
|
119
|
+
# Remove multiple consecutive hyphens
|
|
120
|
+
sanitized = re.sub(r'-+', '-', sanitized)
|
|
121
|
+
# Strip leading/trailing hyphens
|
|
122
|
+
return sanitized.strip("-")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: x-ipe
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.0.25
|
|
4
|
+
Summary: An AI native integrated project environment for end to end business value delivery
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Requires-Dist: beautifulsoup4>=4.14.3
|