htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern Storage for CIGS (Computational Imperative Guidance System).
|
|
3
|
+
|
|
4
|
+
Provides thread-safe storage and retrieval of detected behavioral patterns
|
|
5
|
+
in HtmlGraph format (.htmlgraph/cigs/patterns.json).
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Atomic JSON read/write with file locking
|
|
9
|
+
- Thread-safe operations with lock management
|
|
10
|
+
- Pattern persistence across sessions
|
|
11
|
+
- Pattern analytics and aggregation
|
|
12
|
+
|
|
13
|
+
Reference: .htmlgraph/spikes/computational-imperative-guidance-system-design.md (Part 3.2)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from threading import Lock
|
|
21
|
+
from typing import Any, cast
|
|
22
|
+
from uuid import uuid4
|
|
23
|
+
|
|
24
|
+
from htmlgraph.cigs.models import PatternRecord
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PatternStorage:
|
|
30
|
+
"""
|
|
31
|
+
Thread-safe storage for behavioral patterns in HtmlGraph format.
|
|
32
|
+
|
|
33
|
+
Storage format: `.htmlgraph/cigs/patterns.json`
|
|
34
|
+
|
|
35
|
+
JSON Schema:
|
|
36
|
+
{
|
|
37
|
+
"patterns": [
|
|
38
|
+
{
|
|
39
|
+
"id": "pattern-001",
|
|
40
|
+
"pattern_type": "anti-pattern",
|
|
41
|
+
"name": "Read-Grep-Read Sequence",
|
|
42
|
+
"description": "Multiple exploration tools used in sequence",
|
|
43
|
+
"trigger_conditions": ["3+ exploration tools in last 5 calls"],
|
|
44
|
+
"example_sequence": ["Read", "Grep", "Read"],
|
|
45
|
+
"occurrence_count": 15,
|
|
46
|
+
"sessions_affected": ["sess-abc", "sess-def"],
|
|
47
|
+
"correct_approach": "Use spawn_gemini() for exploration",
|
|
48
|
+
"delegation_suggestion": "spawn_gemini(prompt='...')"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"good_patterns": [...]
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, graph_dir: Path):
|
|
56
|
+
"""
|
|
57
|
+
Initialize pattern storage.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
graph_dir: Path to .htmlgraph directory (e.g., /project/.htmlgraph)
|
|
61
|
+
"""
|
|
62
|
+
self.graph_dir = Path(graph_dir)
|
|
63
|
+
self.patterns_file = self.graph_dir / "cigs" / "patterns.json"
|
|
64
|
+
self._lock = Lock()
|
|
65
|
+
|
|
66
|
+
# Ensure directory exists
|
|
67
|
+
self.patterns_file.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
# Initialize file if it doesn't exist
|
|
70
|
+
if not self.patterns_file.exists():
|
|
71
|
+
self._write_atomic({"patterns": [], "good_patterns": []})
|
|
72
|
+
|
|
73
|
+
def add_pattern(self, pattern: PatternRecord) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Add a new pattern to storage.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
pattern: PatternRecord to add
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Pattern ID (generated if not provided)
|
|
82
|
+
"""
|
|
83
|
+
if not pattern.id:
|
|
84
|
+
pattern.id = f"pattern-{uuid4().hex[:8]}"
|
|
85
|
+
|
|
86
|
+
with self._lock:
|
|
87
|
+
data = self._read_atomic()
|
|
88
|
+
|
|
89
|
+
# Determine which list to add to
|
|
90
|
+
if pattern.pattern_type == "anti-pattern":
|
|
91
|
+
patterns_list = data["patterns"]
|
|
92
|
+
else:
|
|
93
|
+
patterns_list = data["good_patterns"]
|
|
94
|
+
|
|
95
|
+
# Check if pattern already exists
|
|
96
|
+
existing = next((p for p in patterns_list if p["id"] == pattern.id), None)
|
|
97
|
+
|
|
98
|
+
if existing:
|
|
99
|
+
# Update existing pattern
|
|
100
|
+
patterns_list[patterns_list.index(existing)] = self._pattern_to_dict(
|
|
101
|
+
pattern
|
|
102
|
+
)
|
|
103
|
+
logger.debug(f"Updated pattern: {pattern.id}")
|
|
104
|
+
else:
|
|
105
|
+
# Add new pattern
|
|
106
|
+
patterns_list.append(self._pattern_to_dict(pattern))
|
|
107
|
+
logger.debug(f"Added pattern: {pattern.id}")
|
|
108
|
+
|
|
109
|
+
self._write_atomic(data)
|
|
110
|
+
|
|
111
|
+
return pattern.id
|
|
112
|
+
|
|
113
|
+
def get_pattern(self, pattern_id: str) -> PatternRecord | None:
|
|
114
|
+
"""
|
|
115
|
+
Retrieve a pattern by ID.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
pattern_id: Pattern ID to retrieve
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
PatternRecord if found, None otherwise
|
|
122
|
+
"""
|
|
123
|
+
with self._lock:
|
|
124
|
+
data = self._read_atomic()
|
|
125
|
+
|
|
126
|
+
# Search in both lists
|
|
127
|
+
for pattern_dict in data["patterns"] + data["good_patterns"]:
|
|
128
|
+
if pattern_dict["id"] == pattern_id:
|
|
129
|
+
return self._dict_to_pattern(pattern_dict)
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def get_all_patterns(self) -> list[PatternRecord]:
|
|
134
|
+
"""
|
|
135
|
+
Retrieve all patterns (both anti-patterns and good patterns).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of all PatternRecord objects
|
|
139
|
+
"""
|
|
140
|
+
with self._lock:
|
|
141
|
+
data = self._read_atomic()
|
|
142
|
+
|
|
143
|
+
patterns = []
|
|
144
|
+
for pattern_dict in data["patterns"] + data["good_patterns"]:
|
|
145
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
146
|
+
|
|
147
|
+
return patterns
|
|
148
|
+
|
|
149
|
+
def get_anti_patterns(self) -> list[PatternRecord]:
|
|
150
|
+
"""
|
|
151
|
+
Retrieve only anti-patterns.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
List of anti-pattern PatternRecord objects
|
|
155
|
+
"""
|
|
156
|
+
with self._lock:
|
|
157
|
+
data = self._read_atomic()
|
|
158
|
+
|
|
159
|
+
patterns = []
|
|
160
|
+
for pattern_dict in data["patterns"]:
|
|
161
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
162
|
+
|
|
163
|
+
return patterns
|
|
164
|
+
|
|
165
|
+
def get_good_patterns(self) -> list[PatternRecord]:
|
|
166
|
+
"""
|
|
167
|
+
Retrieve only good patterns.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of good pattern PatternRecord objects
|
|
171
|
+
"""
|
|
172
|
+
with self._lock:
|
|
173
|
+
data = self._read_atomic()
|
|
174
|
+
|
|
175
|
+
patterns = []
|
|
176
|
+
for pattern_dict in data["good_patterns"]:
|
|
177
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
178
|
+
|
|
179
|
+
return patterns
|
|
180
|
+
|
|
181
|
+
def update_pattern_occurrence(self, pattern_id: str, session_id: str) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
Update pattern occurrence count and add session.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
pattern_id: Pattern to update
|
|
187
|
+
session_id: Session where pattern was detected
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if updated, False if pattern not found
|
|
191
|
+
"""
|
|
192
|
+
with self._lock:
|
|
193
|
+
data = self._read_atomic()
|
|
194
|
+
|
|
195
|
+
# Search in both lists
|
|
196
|
+
for patterns_list in [data["patterns"], data["good_patterns"]]:
|
|
197
|
+
for pattern_dict in patterns_list:
|
|
198
|
+
if pattern_dict["id"] == pattern_id:
|
|
199
|
+
pattern_dict["occurrence_count"] += 1
|
|
200
|
+
|
|
201
|
+
# Add session if not already present
|
|
202
|
+
if session_id not in pattern_dict["sessions_affected"]:
|
|
203
|
+
pattern_dict["sessions_affected"].append(session_id)
|
|
204
|
+
|
|
205
|
+
self._write_atomic(data)
|
|
206
|
+
logger.debug(
|
|
207
|
+
f"Updated occurrence for pattern {pattern_id}: "
|
|
208
|
+
f"count={pattern_dict['occurrence_count']}"
|
|
209
|
+
)
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def remove_pattern(self, pattern_id: str) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Remove a pattern by ID.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
pattern_id: Pattern ID to remove
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if removed, False if not found
|
|
223
|
+
"""
|
|
224
|
+
with self._lock:
|
|
225
|
+
data = self._read_atomic()
|
|
226
|
+
|
|
227
|
+
# Search and remove from both lists
|
|
228
|
+
for patterns_list in [data["patterns"], data["good_patterns"]]:
|
|
229
|
+
for i, pattern_dict in enumerate(patterns_list):
|
|
230
|
+
if pattern_dict["id"] == pattern_id:
|
|
231
|
+
patterns_list.pop(i)
|
|
232
|
+
self._write_atomic(data)
|
|
233
|
+
logger.debug(f"Removed pattern: {pattern_id}")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def query_patterns(
|
|
239
|
+
self,
|
|
240
|
+
pattern_type: str | None = None,
|
|
241
|
+
min_occurrences: int = 0,
|
|
242
|
+
) -> list[PatternRecord]:
|
|
243
|
+
"""
|
|
244
|
+
Query patterns with filters.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
pattern_type: Filter by type ("anti-pattern", "good-pattern", None for all)
|
|
248
|
+
min_occurrences: Minimum occurrence count to include
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of matching PatternRecord objects
|
|
252
|
+
"""
|
|
253
|
+
with self._lock:
|
|
254
|
+
data = self._read_atomic()
|
|
255
|
+
|
|
256
|
+
patterns = []
|
|
257
|
+
|
|
258
|
+
# Add anti-patterns if requested
|
|
259
|
+
if pattern_type is None or pattern_type == "anti-pattern":
|
|
260
|
+
for pattern_dict in data["patterns"]:
|
|
261
|
+
if pattern_dict["occurrence_count"] >= min_occurrences:
|
|
262
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
263
|
+
|
|
264
|
+
# Add good patterns if requested
|
|
265
|
+
if pattern_type is None or pattern_type == "good-pattern":
|
|
266
|
+
for pattern_dict in data["good_patterns"]:
|
|
267
|
+
if pattern_dict["occurrence_count"] >= min_occurrences:
|
|
268
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
269
|
+
|
|
270
|
+
return patterns
|
|
271
|
+
|
|
272
|
+
def get_patterns_by_session(self, session_id: str) -> list[PatternRecord]:
|
|
273
|
+
"""
|
|
274
|
+
Get all patterns detected in a specific session.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
session_id: Session ID to query
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of patterns detected in that session
|
|
281
|
+
"""
|
|
282
|
+
with self._lock:
|
|
283
|
+
data = self._read_atomic()
|
|
284
|
+
|
|
285
|
+
patterns = []
|
|
286
|
+
for pattern_dict in data["patterns"] + data["good_patterns"]:
|
|
287
|
+
if session_id in pattern_dict["sessions_affected"]:
|
|
288
|
+
patterns.append(self._dict_to_pattern(pattern_dict))
|
|
289
|
+
|
|
290
|
+
return patterns
|
|
291
|
+
|
|
292
|
+
def export_analytics(self) -> dict:
|
|
293
|
+
"""
|
|
294
|
+
Export pattern analytics for reporting.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dictionary with aggregated statistics
|
|
298
|
+
"""
|
|
299
|
+
with self._lock:
|
|
300
|
+
data = self._read_atomic()
|
|
301
|
+
|
|
302
|
+
anti_patterns = data["patterns"]
|
|
303
|
+
good_patterns = data["good_patterns"]
|
|
304
|
+
|
|
305
|
+
total_anti = len(anti_patterns)
|
|
306
|
+
total_good = len(good_patterns)
|
|
307
|
+
total_occurrences = sum(p["occurrence_count"] for p in anti_patterns)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
311
|
+
"summary": {
|
|
312
|
+
"total_anti_patterns": total_anti,
|
|
313
|
+
"total_good_patterns": total_good,
|
|
314
|
+
"total_detections": total_occurrences,
|
|
315
|
+
},
|
|
316
|
+
"anti_patterns": [
|
|
317
|
+
{
|
|
318
|
+
"id": p["id"],
|
|
319
|
+
"name": p["name"],
|
|
320
|
+
"occurrences": p["occurrence_count"],
|
|
321
|
+
"sessions_affected": len(p["sessions_affected"]),
|
|
322
|
+
}
|
|
323
|
+
for p in sorted(
|
|
324
|
+
anti_patterns,
|
|
325
|
+
key=lambda x: -x["occurrence_count"],
|
|
326
|
+
)
|
|
327
|
+
],
|
|
328
|
+
"good_patterns": [
|
|
329
|
+
{
|
|
330
|
+
"id": p["id"],
|
|
331
|
+
"name": p["name"],
|
|
332
|
+
"occurrences": p["occurrence_count"],
|
|
333
|
+
"sessions_affected": len(p["sessions_affected"]),
|
|
334
|
+
}
|
|
335
|
+
for p in sorted(
|
|
336
|
+
good_patterns,
|
|
337
|
+
key=lambda x: -x["occurrence_count"],
|
|
338
|
+
)
|
|
339
|
+
],
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def clear_all(self) -> None:
|
|
343
|
+
"""Clear all patterns from storage. Use with caution."""
|
|
344
|
+
with self._lock:
|
|
345
|
+
self._write_atomic({"patterns": [], "good_patterns": []})
|
|
346
|
+
logger.warning("Cleared all patterns from storage")
|
|
347
|
+
|
|
348
|
+
# Private methods
|
|
349
|
+
|
|
350
|
+
def _read_atomic(self) -> dict[str, list]:
|
|
351
|
+
"""
|
|
352
|
+
Read patterns file atomically.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Dictionary with "patterns" and "good_patterns" keys
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
if not self.patterns_file.exists():
|
|
359
|
+
return {"patterns": [], "good_patterns": []}
|
|
360
|
+
|
|
361
|
+
content = self.patterns_file.read_text(encoding="utf-8")
|
|
362
|
+
data: Any = json.loads(content)
|
|
363
|
+
|
|
364
|
+
# Validate structure
|
|
365
|
+
if "patterns" not in data:
|
|
366
|
+
data["patterns"] = []
|
|
367
|
+
if "good_patterns" not in data:
|
|
368
|
+
data["good_patterns"] = []
|
|
369
|
+
|
|
370
|
+
return cast(dict[str, list], data)
|
|
371
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
372
|
+
logger.error(f"Error reading patterns file: {e}")
|
|
373
|
+
return {"patterns": [], "good_patterns": []}
|
|
374
|
+
|
|
375
|
+
def _write_atomic(self, data: dict[str, list]) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Write patterns file atomically using temp file + rename.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
data: Dictionary with "patterns" and "good_patterns" keys
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
# Write to temporary file first
|
|
384
|
+
temp_file = self.patterns_file.with_suffix(".json.tmp")
|
|
385
|
+
|
|
386
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
387
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
388
|
+
|
|
389
|
+
# Atomic rename
|
|
390
|
+
temp_file.replace(self.patterns_file)
|
|
391
|
+
logger.debug(f"Wrote patterns to {self.patterns_file}")
|
|
392
|
+
|
|
393
|
+
except OSError as e:
|
|
394
|
+
logger.error(f"Error writing patterns file: {e}")
|
|
395
|
+
raise
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def _pattern_to_dict(pattern: PatternRecord) -> dict:
|
|
399
|
+
"""Convert PatternRecord to dictionary for JSON."""
|
|
400
|
+
return {
|
|
401
|
+
"id": pattern.id,
|
|
402
|
+
"pattern_type": pattern.pattern_type,
|
|
403
|
+
"name": pattern.name,
|
|
404
|
+
"description": pattern.description,
|
|
405
|
+
"trigger_conditions": pattern.trigger_conditions,
|
|
406
|
+
"example_sequence": pattern.example_sequence,
|
|
407
|
+
"occurrence_count": pattern.occurrence_count,
|
|
408
|
+
"sessions_affected": pattern.sessions_affected,
|
|
409
|
+
"correct_approach": pattern.correct_approach,
|
|
410
|
+
"delegation_suggestion": pattern.delegation_suggestion,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def _dict_to_pattern(data: dict) -> PatternRecord:
|
|
415
|
+
"""Convert dictionary (from JSON) to PatternRecord."""
|
|
416
|
+
return PatternRecord(
|
|
417
|
+
id=data["id"],
|
|
418
|
+
pattern_type=data["pattern_type"],
|
|
419
|
+
name=data["name"],
|
|
420
|
+
description=data["description"],
|
|
421
|
+
trigger_conditions=data["trigger_conditions"],
|
|
422
|
+
example_sequence=data["example_sequence"],
|
|
423
|
+
occurrence_count=data.get("occurrence_count", 0),
|
|
424
|
+
sessions_affected=data.get("sessions_affected", []),
|
|
425
|
+
correct_approach=data.get("correct_approach"),
|
|
426
|
+
delegation_suggestion=data.get("delegation_suggestion"),
|
|
427
|
+
)
|