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.
Files changed (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. 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
+ }