devforgeai 1.0.4 → 1.0.6
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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
- package/src/claude/skills/researching-market/SKILL.md +2 -1
- package/src/cli/lib/copier.js +13 -1
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration manager for feedback system.
|
|
3
|
+
|
|
4
|
+
This module provides the main interface for loading, validating, and managing
|
|
5
|
+
feedback system configuration from YAML files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Dict, Any
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import yaml
|
|
17
|
+
except ImportError:
|
|
18
|
+
yaml = None
|
|
19
|
+
|
|
20
|
+
from .config_models import FeedbackConfiguration, ConversationSettings, SkipTrackingSettings, TemplateSettings
|
|
21
|
+
from .config_defaults import DEFAULT_CONFIG_DICT, get_default_config
|
|
22
|
+
from .config_schema import get_schema
|
|
23
|
+
from .skip_tracker import SkipTracker
|
|
24
|
+
from .hot_reload import HotReloadManager, ConfigFileWatcher
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigurationManager:
|
|
28
|
+
"""Manages feedback system configuration loading, validation, and updates.
|
|
29
|
+
|
|
30
|
+
Handles YAML parsing, configuration validation, default merging, and
|
|
31
|
+
hot-reload of configuration changes.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
config_file_path: Optional[Path] = None,
|
|
37
|
+
logs_dir: Optional[Path] = None,
|
|
38
|
+
enable_hot_reload: bool = True
|
|
39
|
+
):
|
|
40
|
+
"""Initialize the configuration manager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config_file_path: Path to feedback.yaml configuration file.
|
|
44
|
+
Defaults to devforgeai/config/feedback.yaml
|
|
45
|
+
logs_dir: Directory for logging configuration changes.
|
|
46
|
+
Defaults to devforgeai/logs
|
|
47
|
+
enable_hot_reload: Whether to enable configuration hot-reload.
|
|
48
|
+
"""
|
|
49
|
+
if config_file_path is None:
|
|
50
|
+
config_file_path = Path("devforgeai/config/feedback.yaml")
|
|
51
|
+
if logs_dir is None:
|
|
52
|
+
logs_dir = Path("devforgeai/logs")
|
|
53
|
+
|
|
54
|
+
self.config_file_path = config_file_path
|
|
55
|
+
self.logs_dir = logs_dir
|
|
56
|
+
self._config_errors_log = logs_dir / "config-errors.log"
|
|
57
|
+
self._current_config: Optional[FeedbackConfiguration] = None
|
|
58
|
+
self._skip_tracker: Optional[SkipTracker] = None
|
|
59
|
+
self._hot_reload_manager: Optional[HotReloadManager] = None
|
|
60
|
+
self._initialization_lock = threading.Lock()
|
|
61
|
+
self._initialized = False
|
|
62
|
+
self._debug = os.getenv("DEBUG_FEEDBACK_CONFIG", "false").lower() == "true"
|
|
63
|
+
|
|
64
|
+
# Setup logging
|
|
65
|
+
self._setup_logging()
|
|
66
|
+
|
|
67
|
+
# Load initial configuration
|
|
68
|
+
self._current_config = self.load_configuration()
|
|
69
|
+
|
|
70
|
+
# Initialize skip tracker
|
|
71
|
+
self._skip_tracker = SkipTracker(logs_dir / "feedback-skips.log")
|
|
72
|
+
|
|
73
|
+
# Setup hot-reload if enabled
|
|
74
|
+
if enable_hot_reload:
|
|
75
|
+
self._hot_reload_manager = HotReloadManager(
|
|
76
|
+
config_file_path,
|
|
77
|
+
self._reload_config_callback
|
|
78
|
+
)
|
|
79
|
+
self._hot_reload_manager.start()
|
|
80
|
+
self._hot_reload_manager.set_current_config(self._current_config)
|
|
81
|
+
|
|
82
|
+
self._initialized = True
|
|
83
|
+
|
|
84
|
+
def _setup_logging(self) -> None:
|
|
85
|
+
"""Setup logging for configuration operations."""
|
|
86
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
def _log_error(self, message: str) -> None:
|
|
89
|
+
"""Log configuration error.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
message: Error message to log.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
with open(self._config_errors_log, 'a') as f:
|
|
96
|
+
timestamp = datetime.now().isoformat()
|
|
97
|
+
f.write(f"{timestamp}: {message}\n")
|
|
98
|
+
except (IOError, OSError):
|
|
99
|
+
# Silently fail if log write fails
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
if self._debug:
|
|
103
|
+
print(f"[CONFIG] {message}")
|
|
104
|
+
|
|
105
|
+
def _ensure_config_directory(self) -> None:
|
|
106
|
+
"""Ensure config directory exists."""
|
|
107
|
+
self.config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
def _load_yaml_file(self) -> Optional[Dict[str, Any]]:
|
|
110
|
+
"""Load and parse YAML configuration file.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Parsed configuration dictionary or None if file doesn't exist.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
yaml.YAMLError: If YAML parsing fails.
|
|
117
|
+
IOError: If file cannot be read.
|
|
118
|
+
"""
|
|
119
|
+
if not self.config_file_path.exists():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
if yaml is None:
|
|
124
|
+
self._log_error("PyYAML not available - cannot load configuration")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
with open(self.config_file_path, 'r') as f:
|
|
128
|
+
content = yaml.safe_load(f)
|
|
129
|
+
return content if isinstance(content, dict) else {}
|
|
130
|
+
|
|
131
|
+
except yaml.YAMLError as e:
|
|
132
|
+
self._log_error(f"YAML parsing error: {str(e)}")
|
|
133
|
+
raise
|
|
134
|
+
except (IOError, OSError) as e:
|
|
135
|
+
self._log_error(f"File read error: {str(e)}")
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def _merge_nested_config(self, section_name: str, loaded_config: Dict[str, Any], merged: Dict[str, Any]) -> None:
|
|
139
|
+
"""Merge a nested configuration section with defaults.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
section_name: Name of the configuration section to merge.
|
|
143
|
+
loaded_config: Loaded configuration dictionary.
|
|
144
|
+
merged: Target merged configuration dictionary to update in-place.
|
|
145
|
+
"""
|
|
146
|
+
if section_name in loaded_config and isinstance(loaded_config[section_name], dict):
|
|
147
|
+
merged_section = get_default_config()[section_name].copy()
|
|
148
|
+
merged_section.update(loaded_config[section_name])
|
|
149
|
+
merged[section_name] = merged_section
|
|
150
|
+
|
|
151
|
+
def _merge_with_defaults(self, loaded_config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
152
|
+
"""Merge loaded configuration with defaults.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
loaded_config: Configuration loaded from file (may be None or partial).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Complete configuration with defaults merged.
|
|
159
|
+
"""
|
|
160
|
+
if loaded_config is None:
|
|
161
|
+
return get_default_config()
|
|
162
|
+
|
|
163
|
+
# Start with defaults
|
|
164
|
+
merged = get_default_config()
|
|
165
|
+
|
|
166
|
+
# Override with loaded values (shallow merge at top level)
|
|
167
|
+
merged.update(loaded_config)
|
|
168
|
+
|
|
169
|
+
# For nested objects, merge more carefully
|
|
170
|
+
for section_name in ("conversation_settings", "skip_tracking", "templates"):
|
|
171
|
+
self._merge_nested_config(section_name, loaded_config, merged)
|
|
172
|
+
|
|
173
|
+
return merged
|
|
174
|
+
|
|
175
|
+
def _parse_nested_settings(self, section_name: str, config_dict: Dict[str, Any], settings_class: type) -> Optional[Any]:
|
|
176
|
+
"""Parse a nested configuration section to a dataclass object.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
section_name: Name of the configuration section.
|
|
180
|
+
config_dict: Configuration dictionary.
|
|
181
|
+
settings_class: Dataclass to instantiate (ConversationSettings, SkipTrackingSettings, or TemplateSettings).
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Instantiated settings object or None if section not found.
|
|
185
|
+
"""
|
|
186
|
+
if section_name in config_dict:
|
|
187
|
+
section_dict = config_dict[section_name]
|
|
188
|
+
if isinstance(section_dict, dict):
|
|
189
|
+
return settings_class(**section_dict)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _dict_to_configuration(self, config_dict: Dict[str, Any]) -> FeedbackConfiguration:
|
|
193
|
+
"""Convert configuration dictionary to FeedbackConfiguration object.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
config_dict: Configuration dictionary.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
FeedbackConfiguration object.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If configuration is invalid.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
# Parse nested objects
|
|
206
|
+
conv_settings = self._parse_nested_settings("conversation_settings", config_dict, ConversationSettings)
|
|
207
|
+
skip_settings = self._parse_nested_settings("skip_tracking", config_dict, SkipTrackingSettings)
|
|
208
|
+
template_settings = self._parse_nested_settings("templates", config_dict, TemplateSettings)
|
|
209
|
+
|
|
210
|
+
# Create configuration object
|
|
211
|
+
config = FeedbackConfiguration(
|
|
212
|
+
enabled=config_dict.get("enabled", True),
|
|
213
|
+
trigger_mode=config_dict.get("trigger_mode", "failures-only"),
|
|
214
|
+
operations=config_dict.get("operations"),
|
|
215
|
+
conversation_settings=conv_settings,
|
|
216
|
+
skip_tracking=skip_settings,
|
|
217
|
+
templates=template_settings
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return config
|
|
221
|
+
|
|
222
|
+
except ValueError as e:
|
|
223
|
+
self._log_error(f"Configuration validation error: {str(e)}")
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
def load_configuration(self) -> FeedbackConfiguration:
|
|
227
|
+
"""Load and validate feedback configuration.
|
|
228
|
+
|
|
229
|
+
Loads YAML file, validates structure, merges with defaults, and
|
|
230
|
+
returns a FeedbackConfiguration object.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
FeedbackConfiguration object with loaded or default values.
|
|
234
|
+
|
|
235
|
+
Performance:
|
|
236
|
+
Should complete in <100ms.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
# Load YAML file
|
|
240
|
+
loaded_dict = self._load_yaml_file()
|
|
241
|
+
|
|
242
|
+
# Merge with defaults
|
|
243
|
+
merged_config = self._merge_with_defaults(loaded_dict)
|
|
244
|
+
|
|
245
|
+
# Convert to configuration object (validates)
|
|
246
|
+
config = self._dict_to_configuration(merged_config)
|
|
247
|
+
|
|
248
|
+
return config
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
self._log_error(f"Failed to load configuration: {str(e)}")
|
|
252
|
+
# Return default configuration on error
|
|
253
|
+
return FeedbackConfiguration()
|
|
254
|
+
|
|
255
|
+
def _reload_config_callback(self) -> FeedbackConfiguration:
|
|
256
|
+
"""Callback for hot-reload to load new configuration.
|
|
257
|
+
|
|
258
|
+
Called when configuration file changes are detected.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
New FeedbackConfiguration object.
|
|
262
|
+
"""
|
|
263
|
+
return self.load_configuration()
|
|
264
|
+
|
|
265
|
+
def get_configuration(self) -> FeedbackConfiguration:
|
|
266
|
+
"""Get the current configuration.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Current FeedbackConfiguration object.
|
|
270
|
+
"""
|
|
271
|
+
with self._initialization_lock:
|
|
272
|
+
if self._current_config is None:
|
|
273
|
+
self._current_config = self.load_configuration()
|
|
274
|
+
return self._current_config
|
|
275
|
+
|
|
276
|
+
def update_configuration(self, config: FeedbackConfiguration) -> None:
|
|
277
|
+
"""Update the current configuration (for testing/reloading).
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
config: New FeedbackConfiguration object.
|
|
281
|
+
"""
|
|
282
|
+
with self._initialization_lock:
|
|
283
|
+
self._current_config = config
|
|
284
|
+
|
|
285
|
+
def is_enabled(self) -> bool:
|
|
286
|
+
"""Check if feedback collection is enabled.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
True if enabled, False otherwise.
|
|
290
|
+
"""
|
|
291
|
+
config = self.get_configuration()
|
|
292
|
+
return config.enabled
|
|
293
|
+
|
|
294
|
+
def get_trigger_mode(self) -> str:
|
|
295
|
+
"""Get the trigger mode.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Trigger mode string (always, failures-only, specific-operations, never).
|
|
299
|
+
"""
|
|
300
|
+
config = self.get_configuration()
|
|
301
|
+
return config.trigger_mode
|
|
302
|
+
|
|
303
|
+
def get_operations(self) -> Optional[list]:
|
|
304
|
+
"""Get the list of specific operations (if applicable).
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List of operation names or None.
|
|
308
|
+
"""
|
|
309
|
+
config = self.get_configuration()
|
|
310
|
+
return config.operations
|
|
311
|
+
|
|
312
|
+
def _get_nested_config(self, attribute_name: str) -> Optional[Any]:
|
|
313
|
+
"""Get a nested configuration object by attribute name.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
attribute_name: Name of the nested config attribute
|
|
317
|
+
(conversation_settings, skip_tracking, or templates).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The nested configuration object or None if not found.
|
|
321
|
+
"""
|
|
322
|
+
config = self.get_configuration()
|
|
323
|
+
return getattr(config, attribute_name, None)
|
|
324
|
+
|
|
325
|
+
def get_conversation_settings(self) -> Optional[ConversationSettings]:
|
|
326
|
+
"""Get conversation settings from current configuration.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
ConversationSettings object or None.
|
|
330
|
+
"""
|
|
331
|
+
return self._get_nested_config("conversation_settings")
|
|
332
|
+
|
|
333
|
+
def get_skip_tracking_settings(self) -> Optional[SkipTrackingSettings]:
|
|
334
|
+
"""Get skip tracking settings from current configuration.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
SkipTrackingSettings object or None.
|
|
338
|
+
"""
|
|
339
|
+
return self._get_nested_config("skip_tracking")
|
|
340
|
+
|
|
341
|
+
def get_template_settings(self) -> Optional[TemplateSettings]:
|
|
342
|
+
"""Get template settings from current configuration.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
TemplateSettings object or None.
|
|
346
|
+
"""
|
|
347
|
+
return self._get_nested_config("templates")
|
|
348
|
+
|
|
349
|
+
def get_skip_tracker(self) -> SkipTracker:
|
|
350
|
+
"""Get the skip tracker instance.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
SkipTracker object for tracking skips.
|
|
354
|
+
"""
|
|
355
|
+
return self._skip_tracker
|
|
356
|
+
|
|
357
|
+
def is_hot_reload_enabled(self) -> bool:
|
|
358
|
+
"""Check if hot-reload is active.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if hot-reload is running, False otherwise.
|
|
362
|
+
"""
|
|
363
|
+
if self._hot_reload_manager is None:
|
|
364
|
+
return False
|
|
365
|
+
return self._hot_reload_manager.is_running()
|
|
366
|
+
|
|
367
|
+
def start_hot_reload(self) -> None:
|
|
368
|
+
"""Start configuration hot-reload."""
|
|
369
|
+
if self._hot_reload_manager is not None and not self._hot_reload_manager.is_running():
|
|
370
|
+
self._hot_reload_manager.start()
|
|
371
|
+
self._hot_reload_manager.set_current_config(self._current_config)
|
|
372
|
+
|
|
373
|
+
def stop_hot_reload(self) -> None:
|
|
374
|
+
"""Stop configuration hot-reload."""
|
|
375
|
+
if self._hot_reload_manager is not None:
|
|
376
|
+
self._hot_reload_manager.stop()
|
|
377
|
+
|
|
378
|
+
def shutdown(self) -> None:
|
|
379
|
+
"""Shutdown the configuration manager (stop hot-reload, etc.)."""
|
|
380
|
+
self.stop_hot_reload()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Global configuration manager instance
|
|
384
|
+
_global_config_manager: Optional[ConfigurationManager] = None
|
|
385
|
+
_global_manager_lock = threading.Lock()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_config_manager(
|
|
389
|
+
config_file_path: Optional[Path] = None,
|
|
390
|
+
logs_dir: Optional[Path] = None
|
|
391
|
+
) -> ConfigurationManager:
|
|
392
|
+
"""Get or create the global configuration manager.
|
|
393
|
+
|
|
394
|
+
Uses singleton pattern to ensure only one manager exists.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
config_file_path: Path to configuration file (used on first call only).
|
|
398
|
+
logs_dir: Path to logs directory (used on first call only).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
ConfigurationManager instance.
|
|
402
|
+
"""
|
|
403
|
+
global _global_config_manager
|
|
404
|
+
|
|
405
|
+
if _global_config_manager is None:
|
|
406
|
+
with _global_manager_lock:
|
|
407
|
+
if _global_config_manager is None:
|
|
408
|
+
_global_config_manager = ConfigurationManager(
|
|
409
|
+
config_file_path=config_file_path,
|
|
410
|
+
logs_dir=logs_dir
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return _global_config_manager
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def reset_config_manager() -> None:
|
|
417
|
+
"""Reset the global configuration manager (for testing)."""
|
|
418
|
+
global _global_config_manager
|
|
419
|
+
|
|
420
|
+
with _global_manager_lock:
|
|
421
|
+
if _global_config_manager is not None:
|
|
422
|
+
_global_config_manager.shutdown()
|
|
423
|
+
_global_config_manager = None
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for feedback system configuration management.
|
|
3
|
+
|
|
4
|
+
This module provides dataclass definitions for all configuration structures,
|
|
5
|
+
including validation in __post_init__ methods.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from typing import List, Optional, Set
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TriggerMode(Enum):
|
|
14
|
+
"""Trigger modes for feedback collection."""
|
|
15
|
+
ALWAYS = "always"
|
|
16
|
+
FAILURES_ONLY = "failures-only"
|
|
17
|
+
SPECIFIC_OPS = "specific-operations"
|
|
18
|
+
NEVER = "never"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TemplateFormat(Enum):
|
|
22
|
+
"""Template format options for feedback collection."""
|
|
23
|
+
STRUCTURED = "structured"
|
|
24
|
+
FREE_TEXT = "free-text"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TemplateTone(Enum):
|
|
28
|
+
"""Template tone options for feedback questions."""
|
|
29
|
+
BRIEF = "brief"
|
|
30
|
+
DETAILED = "detailed"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Validation constants
|
|
34
|
+
VALID_TEMPLATE_FORMATS: Set[str] = {"structured", "free-text"}
|
|
35
|
+
VALID_TEMPLATE_TONES: Set[str] = {"brief", "detailed"}
|
|
36
|
+
VALID_TRIGGER_MODES: Set[str] = {"always", "failures-only", "specific-operations", "never"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ConversationSettings:
|
|
41
|
+
"""Conversation-level settings for feedback collection.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
max_questions: Maximum number of questions per conversation. 0 = unlimited.
|
|
45
|
+
allow_skip: Whether users can skip feedback questions.
|
|
46
|
+
"""
|
|
47
|
+
max_questions: int = 5
|
|
48
|
+
allow_skip: bool = True
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
"""Validate conversation settings."""
|
|
52
|
+
if not isinstance(self.max_questions, int) or self.max_questions < 0:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"max_questions must be non-negative integer, got {self.max_questions}"
|
|
55
|
+
)
|
|
56
|
+
if not isinstance(self.allow_skip, bool):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"allow_skip must be boolean, got {self.allow_skip}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class SkipTrackingSettings:
|
|
64
|
+
"""Skip tracking configuration.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
enabled: Whether skip tracking is active.
|
|
68
|
+
max_consecutive_skips: Maximum consecutive skips allowed. 0 = unlimited.
|
|
69
|
+
reset_on_positive: Whether to reset counter on positive feedback.
|
|
70
|
+
"""
|
|
71
|
+
enabled: bool = True
|
|
72
|
+
max_consecutive_skips: int = 3
|
|
73
|
+
reset_on_positive: bool = True
|
|
74
|
+
|
|
75
|
+
def __post_init__(self):
|
|
76
|
+
"""Validate skip tracking settings."""
|
|
77
|
+
if not isinstance(self.enabled, bool):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"enabled must be boolean, got {self.enabled}"
|
|
80
|
+
)
|
|
81
|
+
if not isinstance(self.max_consecutive_skips, int) or self.max_consecutive_skips < 0:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"max_consecutive_skips must be non-negative integer, got {self.max_consecutive_skips}"
|
|
84
|
+
)
|
|
85
|
+
if not isinstance(self.reset_on_positive, bool):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"reset_on_positive must be boolean, got {self.reset_on_positive}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class TemplateSettings:
|
|
93
|
+
"""Template preferences for feedback collection.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
format: Template format (structured or free-text).
|
|
97
|
+
tone: Template tone (brief or detailed).
|
|
98
|
+
"""
|
|
99
|
+
format: str = "structured" # structured|free-text
|
|
100
|
+
tone: str = "brief" # brief|detailed
|
|
101
|
+
|
|
102
|
+
def __post_init__(self):
|
|
103
|
+
"""Validate template settings."""
|
|
104
|
+
if self.format not in VALID_TEMPLATE_FORMATS:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Invalid template format: '{self.format}'. "
|
|
107
|
+
f"Must be one of: {', '.join(sorted(VALID_TEMPLATE_FORMATS))}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if self.tone not in VALID_TEMPLATE_TONES:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Invalid template tone: '{self.tone}'. "
|
|
113
|
+
f"Must be one of: {', '.join(sorted(VALID_TEMPLATE_TONES))}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class FeedbackConfiguration:
|
|
119
|
+
"""Complete feedback system configuration.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
enabled: Master enable/disable switch for all feedback operations.
|
|
123
|
+
trigger_mode: Determines when feedback is collected.
|
|
124
|
+
operations: List of specific operations (only if trigger_mode is specific-operations).
|
|
125
|
+
conversation_settings: Conversation-level settings.
|
|
126
|
+
skip_tracking: Skip tracking configuration.
|
|
127
|
+
templates: Template preferences.
|
|
128
|
+
"""
|
|
129
|
+
enabled: bool = True
|
|
130
|
+
trigger_mode: str = "failures-only"
|
|
131
|
+
operations: Optional[List[str]] = None
|
|
132
|
+
conversation_settings: Optional[ConversationSettings] = None
|
|
133
|
+
skip_tracking: Optional[SkipTrackingSettings] = None
|
|
134
|
+
templates: Optional[TemplateSettings] = None
|
|
135
|
+
|
|
136
|
+
def _normalize_nested_objects(self) -> None:
|
|
137
|
+
"""Convert dict to dataclass objects if needed and ensure defaults."""
|
|
138
|
+
if isinstance(self.conversation_settings, dict):
|
|
139
|
+
self.conversation_settings = ConversationSettings(**self.conversation_settings)
|
|
140
|
+
elif self.conversation_settings is None:
|
|
141
|
+
self.conversation_settings = ConversationSettings()
|
|
142
|
+
|
|
143
|
+
if isinstance(self.skip_tracking, dict):
|
|
144
|
+
self.skip_tracking = SkipTrackingSettings(**self.skip_tracking)
|
|
145
|
+
elif self.skip_tracking is None:
|
|
146
|
+
self.skip_tracking = SkipTrackingSettings()
|
|
147
|
+
|
|
148
|
+
if isinstance(self.templates, dict):
|
|
149
|
+
self.templates = TemplateSettings(**self.templates)
|
|
150
|
+
elif self.templates is None:
|
|
151
|
+
self.templates = TemplateSettings()
|
|
152
|
+
|
|
153
|
+
def _validate_trigger_mode(self) -> None:
|
|
154
|
+
"""Validate trigger_mode field."""
|
|
155
|
+
if self.trigger_mode not in VALID_TRIGGER_MODES:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Invalid trigger_mode value: '{self.trigger_mode}'. "
|
|
158
|
+
f"Must be one of: {', '.join(sorted(VALID_TRIGGER_MODES))}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _validate_enabled(self) -> None:
|
|
162
|
+
"""Validate enabled field."""
|
|
163
|
+
if not isinstance(self.enabled, bool):
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"enabled must be boolean, got {self.enabled}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _validate_operations(self) -> None:
|
|
169
|
+
"""Validate operations field (required only for specific-operations mode)."""
|
|
170
|
+
if self.trigger_mode == "specific-operations":
|
|
171
|
+
if self.operations is None or not isinstance(self.operations, list) or len(self.operations) == 0:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"operations list must be provided and non-empty when trigger_mode is 'specific-operations'"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __post_init__(self):
|
|
177
|
+
"""Initialize nested objects with defaults and validate configuration."""
|
|
178
|
+
self._normalize_nested_objects()
|
|
179
|
+
self._validate_enabled()
|
|
180
|
+
self._validate_trigger_mode()
|
|
181
|
+
self._validate_operations()
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> dict:
|
|
184
|
+
"""Convert configuration to dictionary."""
|
|
185
|
+
return {
|
|
186
|
+
"enabled": self.enabled,
|
|
187
|
+
"trigger_mode": self.trigger_mode,
|
|
188
|
+
"operations": self.operations,
|
|
189
|
+
"conversation_settings": asdict(self.conversation_settings),
|
|
190
|
+
"skip_tracking": asdict(self.skip_tracking),
|
|
191
|
+
"templates": asdict(self.templates),
|
|
192
|
+
}
|