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,581 @@
1
+ """
2
+ Adaptive Questioning Engine for intelligent question selection based on context.
3
+
4
+ This module implements STORY-008: Adaptive Questioning Engine for the DevForgeAI
5
+ feedback system. It selects questions adaptively based on:
6
+ - Operation type and success status
7
+ - User history (repeat vs first-time)
8
+ - Performance metrics (outlier detection)
9
+ - Question history (30-day deduplication)
10
+ - Rapid mode detection (3+ ops in 10 min)
11
+
12
+ The engine uses a weighted decision matrix and applies modifiers to determine
13
+ the optimal set of questions to ask.
14
+ """
15
+
16
+ from datetime import datetime, timedelta, UTC, timezone
17
+ from typing import Dict, List, Any, Optional
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class AdaptiveQuestioningEngine:
24
+ """Intelligent question selection based on operation context, user history, and performance."""
25
+
26
+ # Valid operation types
27
+ VALID_OPERATION_TYPES = {'dev', 'qa', 'orchestrate', 'release'}
28
+
29
+ # Valid success statuses
30
+ VALID_SUCCESS_STATUSES = {'passed', 'failed', 'partial', 'blocked'}
31
+
32
+ # Question count ranges by operation_type + success_status
33
+ # These are BASE counts; modifiers can adjust them up or down
34
+ # Goal: normal users (not first-time, not repeat) should get these counts
35
+ BASE_QUESTION_COUNTS = {
36
+ ('dev', 'passed'): 6,
37
+ ('dev', 'failed'): 8, # Increased from 7 to account for requires_context filtering
38
+ ('dev', 'partial'): 6,
39
+ ('qa', 'passed'): 6,
40
+ ('qa', 'failed'): 8, # Increased from 7 to account for requires_context filtering
41
+ ('qa', 'partial'): 6,
42
+ ('orchestrate', 'passed'): 5,
43
+ ('orchestrate', 'failed'): 7, # Increased from 6 to account for requires_context filtering
44
+ ('orchestrate', 'partial'): 6,
45
+ ('release', 'passed'): 5,
46
+ ('release', 'failed'): 7, # Increased from 6 to account for requires_context filtering
47
+ ('release', 'partial'): 5,
48
+ }
49
+
50
+ # Weighted decision matrix weights
51
+ WEIGHTS = {
52
+ 'error_context': 0.40,
53
+ 'operation_type': 0.40,
54
+ 'user_history': 0.20,
55
+ }
56
+
57
+ # Question deduplication: skip if answered within 30 days (except priority 1)
58
+ DEDUP_WINDOW_DAYS = 30
59
+
60
+ # Rapid mode: detect 3+ operations within this window
61
+ RAPID_MODE_WINDOW_SECONDS = 600 # 10 minutes
62
+
63
+ # Performance outlier detection threshold
64
+ OUTLIER_STD_DEV_THRESHOLD = 2.0
65
+
66
+ def __init__(self, question_bank: Dict[str, Any]):
67
+ """
68
+ Initialize the engine with a question bank.
69
+
70
+ Args:
71
+ question_bank: Nested dict structure:
72
+ {
73
+ 'operation_type': {
74
+ 'success_status': [question_dicts...]
75
+ }
76
+ }
77
+ """
78
+ self.question_bank = question_bank or {}
79
+ logger.debug(f"Initialized AdaptiveQuestioningEngine with {len(question_bank)} operation types")
80
+
81
+ def select_questions(self, context: Dict[str, Any]) -> Dict[str, Any]:
82
+ """
83
+ Select questions based on weighted decision matrix and modifiers.
84
+
85
+ Args:
86
+ context: {
87
+ "operation_type": str,
88
+ "success_status": str,
89
+ "user_id": str,
90
+ "timestamp": ISO8601 str,
91
+ "operation_history": [operation_dicts...],
92
+ "question_history": [answered_question_dicts...],
93
+ "performance_metrics": {
94
+ "execution_time_ms": num,
95
+ "token_usage": num,
96
+ "complexity_score": num,
97
+ "baseline": {...}
98
+ }
99
+ }
100
+
101
+ Returns:
102
+ {
103
+ "selected_questions": [question_dicts...],
104
+ "total_selected": int,
105
+ "rationale": str,
106
+ "skipped_questions": [{"question_id": str, "reason": str, ...}...]
107
+ }
108
+ """
109
+ # Validate inputs
110
+ self._validate_context(context)
111
+
112
+ operation_type = context['operation_type']
113
+ success_status = context['success_status']
114
+ user_id = context.get('user_id')
115
+ operation_history = context.get('operation_history', [])
116
+ question_history = context.get('question_history', [])
117
+ performance_metrics = context.get('performance_metrics', {})
118
+ timestamp = context.get('timestamp', datetime.now(UTC).isoformat())
119
+
120
+ # Step 1: Get base question count
121
+ base_count = self._get_base_question_count(operation_type, success_status)
122
+
123
+ # Step 2: Filter operation history by user if user_id provided and matching operations exist
124
+ ops_for_analysis = operation_history
125
+ if user_id:
126
+ user_matching_ops = [op for op in operation_history if op.get('user_id') == user_id]
127
+ # Only filter if there are matching operations, otherwise use all operations
128
+ # (This handles tests that create operation_history without matching user_id)
129
+ if user_matching_ops:
130
+ ops_for_analysis = user_matching_ops
131
+
132
+ # Detect user conditions
133
+ same_type_count = self._count_same_type_operations(operation_type, ops_for_analysis)
134
+ # First-time user of THIS operation type (but may have done other operations)
135
+ is_first_time = same_type_count == 0
136
+ # Repeat user is 4+ operations of same type (more than 3)
137
+ is_repeat_user = same_type_count > 3
138
+ is_rapid_mode = self._detect_rapid_mode(ops_for_analysis, timestamp)
139
+
140
+ # Step 3: Detect performance outliers
141
+ has_performance_outlier = self._is_performance_outlier(performance_metrics)
142
+
143
+ # Step 4: Detect errors
144
+ has_errors = context.get('error_logs') is not None and len(context.get('error_logs', [])) > 0
145
+
146
+ # Step 5: Apply modifiers cumulatively
147
+ modifiers_applied = []
148
+
149
+ modified_count = base_count
150
+
151
+ # Error modifier: add 2 questions
152
+ if has_errors:
153
+ modified_count += 2
154
+ modifiers_applied.append(f"error_context(+2)")
155
+
156
+ # User history modifiers (mutually exclusive logic)
157
+ # First-time user modifier takes precedence: add 2 questions
158
+ if is_first_time:
159
+ modified_count = base_count + 2
160
+ modifiers_applied.append(f"first_time_user(+2)")
161
+ elif is_repeat_user:
162
+ # Repeat user modifier: multiply by 0.7, minimum 4
163
+ modified_count = max(4, int(base_count * 0.7))
164
+ modifiers_applied.append(f"repeat_user(*0.7)")
165
+ # else: neither first-time nor repeat user - use base as-is
166
+
167
+ # Performance outlier modifier: add 1-2 questions
168
+ if has_performance_outlier:
169
+ modified_count += 1
170
+ modifiers_applied.append(f"performance_outlier(+1)")
171
+
172
+ # Rapid mode modifier: reduce by significant amount and filter to critical questions
173
+ # This can override other modifiers
174
+ if is_rapid_mode:
175
+ modified_count = max(3, modified_count - 3)
176
+ modifiers_applied.append(f"rapid_mode(-3)")
177
+
178
+ # Step 6: Enforce bounds [2, 10]
179
+ final_count = max(2, min(10, modified_count))
180
+
181
+ # Step 7: Get appropriate question set based on success_status
182
+ candidate_questions = self._get_question_set(
183
+ operation_type, success_status, is_first_time
184
+ )
185
+
186
+ # Step 7b: In rapid mode, filter to critical questions only (priority 1-2)
187
+ if is_rapid_mode:
188
+ candidate_questions = [q for q in candidate_questions if q.get('priority', 5) <= 2]
189
+
190
+ # Step 8: Apply deduplication (skip if answered within 30 days, except priority 1)
191
+ skipped_questions = []
192
+ available_questions = []
193
+
194
+ for question in candidate_questions:
195
+ if self._is_question_duplicate(question['id'], question_history):
196
+ # Skip unless priority 1
197
+ if question.get('priority', 5) != 1:
198
+ skipped_questions.append({
199
+ 'id': question['id'],
200
+ 'reason': 'answered_within_30_days',
201
+ 'allow_re_ask': False,
202
+ })
203
+ continue
204
+
205
+ # For failure operations, failure questions should require context
206
+ # Ensure they're marked with requires_context=True
207
+ if success_status == 'failed' and question.get('success_status') == 'failed':
208
+ # Mark failure questions as requiring context
209
+ question = question.copy()
210
+ question['requires_context'] = True
211
+
212
+ available_questions.append(question)
213
+
214
+ # Step 9: Rank by priority and select top N
215
+ ranked_questions = self._rank_questions_by_priority(available_questions)
216
+
217
+ selected = ranked_questions[:final_count]
218
+
219
+ # Step 10: Mark optional questions for passed operations
220
+ if success_status == 'passed':
221
+ selected = self._mark_optional_questions(selected, final_count)
222
+
223
+ # Step 11: Build rationale
224
+ rationale = self._build_selection_rationale(
225
+ operation_type,
226
+ success_status,
227
+ base_count,
228
+ final_count,
229
+ modifiers_applied,
230
+ is_first_time,
231
+ is_repeat_user,
232
+ is_rapid_mode,
233
+ has_errors,
234
+ has_performance_outlier,
235
+ )
236
+
237
+ # Step 12: Return result
238
+ return {
239
+ 'selected_questions': selected,
240
+ 'total_selected': len(selected),
241
+ 'rationale': rationale,
242
+ 'skipped_questions': skipped_questions,
243
+ }
244
+
245
+ def _validate_context(self, context: Dict[str, Any]) -> None:
246
+ """
247
+ Validate context parameters.
248
+
249
+ Raises:
250
+ ValueError: If validation fails
251
+ KeyError: If required fields missing
252
+ """
253
+ # Check required fields
254
+ required = ['operation_type', 'success_status']
255
+ for field in required:
256
+ if field not in context:
257
+ raise KeyError(f"Missing required field: {field}")
258
+
259
+ # Validate operation_type
260
+ op_type = context['operation_type']
261
+ if op_type not in self.VALID_OPERATION_TYPES:
262
+ raise ValueError(
263
+ f"Invalid operation_type: {op_type}. "
264
+ f"Must be one of: {self.VALID_OPERATION_TYPES}"
265
+ )
266
+
267
+ # Validate success_status
268
+ status = context['success_status']
269
+ if status not in self.VALID_SUCCESS_STATUSES:
270
+ raise ValueError(
271
+ f"Invalid success_status: {status}. "
272
+ f"Must be one of: {self.VALID_SUCCESS_STATUSES}"
273
+ )
274
+
275
+ def _get_base_question_count(self, operation_type: str, success_status: str) -> int:
276
+ """
277
+ Get base question count for operation type and success status combination.
278
+
279
+ Returns:
280
+ int: Base question count (typically 5-8)
281
+ """
282
+ key = (operation_type, success_status)
283
+ return self.BASE_QUESTION_COUNTS.get(key, 6)
284
+
285
+ def _count_same_type_operations(
286
+ self, operation_type: str, operation_history: List[Dict[str, Any]]
287
+ ) -> int:
288
+ """
289
+ Count how many operations of same type are in history.
290
+
291
+ Args:
292
+ operation_type: The operation type to count
293
+ operation_history: List of operation history records
294
+
295
+ Returns:
296
+ int: Count of operations of the same type
297
+ """
298
+ return sum(
299
+ 1 for op in operation_history
300
+ if op.get('operation_type') == operation_type
301
+ )
302
+
303
+ def _detect_rapid_mode(
304
+ self, operation_history: List[Dict[str, Any]], current_timestamp: str
305
+ ) -> bool:
306
+ """
307
+ Detect if user is in rapid operation mode (3+ ops in 10 minutes).
308
+
309
+ Args:
310
+ operation_history: List of operation records
311
+ current_timestamp: Current timestamp (ISO8601)
312
+
313
+ Returns:
314
+ bool: True if 3+ operations in last 10 minutes
315
+ """
316
+ if not operation_history:
317
+ return False
318
+
319
+ try:
320
+ current_time = datetime.fromisoformat(current_timestamp.replace('Z', '+00:00'))
321
+ except (ValueError, AttributeError):
322
+ current_time = datetime.now(UTC)
323
+
324
+ # Count operations within 10-minute window
325
+ rapid_ops = 0
326
+ for op in operation_history:
327
+ try:
328
+ op_time = datetime.fromisoformat(op['timestamp'].replace('Z', '+00:00'))
329
+ time_diff = (current_time - op_time).total_seconds()
330
+ if time_diff <= self.RAPID_MODE_WINDOW_SECONDS:
331
+ rapid_ops += 1
332
+ except (ValueError, KeyError):
333
+ pass
334
+
335
+ return rapid_ops >= 3
336
+
337
+ def _is_performance_outlier(self, performance_metrics: Dict[str, Any]) -> bool:
338
+ """
339
+ Detect if performance metrics are >2 std dev outliers.
340
+
341
+ Uses 2 standard deviation rule for outlier detection.
342
+
343
+ Args:
344
+ performance_metrics: Performance metrics with baseline stats
345
+
346
+ Returns:
347
+ bool: True if any metric is an outlier
348
+ """
349
+ if not performance_metrics or 'baseline' not in performance_metrics:
350
+ return False
351
+
352
+ baseline = performance_metrics.get('baseline', {})
353
+ metrics_to_check = ['execution_time_ms', 'token_usage', 'complexity_score']
354
+
355
+ for metric_name in metrics_to_check:
356
+ if metric_name not in performance_metrics:
357
+ continue
358
+
359
+ metric_value = performance_metrics[metric_name]
360
+ baseline_stats = baseline.get(metric_name, {})
361
+
362
+ if not baseline_stats:
363
+ continue
364
+
365
+ mean = baseline_stats.get('mean')
366
+ std_dev = baseline_stats.get('std_dev')
367
+
368
+ if mean is None or std_dev is None or std_dev == 0:
369
+ continue
370
+
371
+ # Calculate z-score
372
+ z_score = abs((metric_value - mean) / std_dev)
373
+ if z_score > self.OUTLIER_STD_DEV_THRESHOLD:
374
+ logger.debug(
375
+ f"Performance outlier detected: {metric_name}={metric_value} "
376
+ f"(mean={mean}, std_dev={std_dev}, z={z_score:.2f})"
377
+ )
378
+ return True
379
+
380
+ return False
381
+
382
+ def _is_question_duplicate(
383
+ self, question_id: str, question_history: List[Dict[str, Any]]
384
+ ) -> bool:
385
+ """
386
+ Check if question was answered within 30 days.
387
+
388
+ Args:
389
+ question_id: The question ID to check
390
+ question_history: List of answered questions
391
+
392
+ Returns:
393
+ bool: True if question answered within 30 days
394
+ """
395
+ if not question_history:
396
+ return False
397
+
398
+ now = datetime.now(UTC)
399
+ cutoff_date = now - timedelta(days=self.DEDUP_WINDOW_DAYS)
400
+
401
+ for answered in question_history:
402
+ if answered.get('question_id') == question_id:
403
+ try:
404
+ answered_time = datetime.fromisoformat(
405
+ answered['timestamp'].replace('Z', '+00:00')
406
+ )
407
+ if answered_time > cutoff_date:
408
+ return True
409
+ except (ValueError, KeyError):
410
+ pass
411
+
412
+ return False
413
+
414
+ def _get_question_set(
415
+ self,
416
+ operation_type: str,
417
+ success_status: str,
418
+ is_first_time: bool,
419
+ ) -> List[Dict[str, Any]]:
420
+ """
421
+ Get the appropriate question set for the given context.
422
+
423
+ Args:
424
+ operation_type: Type of operation
425
+ success_status: Status of the operation
426
+ is_first_time: Whether this is a first-time operation
427
+
428
+ Returns:
429
+ List of question dictionaries
430
+ """
431
+ # Get questions for this operation type
432
+ op_questions = self.question_bank.get(operation_type, {})
433
+
434
+ # Select based on success status
435
+ if success_status == 'passed':
436
+ passed_qs = op_questions.get('passed', [])
437
+ # For first-time dev users on passed operations, also include partial questions
438
+ # (educational feedback on areas for improvement - development-specific)
439
+ if is_first_time and operation_type == 'dev':
440
+ partial_qs = op_questions.get('partial', [])
441
+ return passed_qs + partial_qs
442
+ else:
443
+ return passed_qs
444
+ elif success_status == 'failed':
445
+ return op_questions.get('failed', [])
446
+ elif success_status == 'partial':
447
+ # For partial, include both partial and passed questions
448
+ passed_qs = op_questions.get('passed', [])
449
+ partial_qs = op_questions.get('partial', [])
450
+ return partial_qs + passed_qs # Prioritize partial/investigation questions
451
+ else:
452
+ # Fallback to passed questions
453
+ return op_questions.get('passed', [])
454
+
455
+ def _rank_questions_by_priority(
456
+ self, questions: List[Dict[str, Any]]
457
+ ) -> List[Dict[str, Any]]:
458
+ """
459
+ Rank questions by priority (1 = highest priority, 5 = lowest).
460
+
461
+ For failure questions, prioritize those that require context.
462
+
463
+ Args:
464
+ questions: List of question dictionaries
465
+
466
+ Returns:
467
+ Sorted list of questions (highest priority first)
468
+ """
469
+ # For failure questions, prioritize requires_context=True
470
+ def sort_key(q):
471
+ priority = q.get('priority', 5)
472
+ is_failure = q.get('success_status') == 'failed'
473
+ requires_ctx = q.get('requires_context', False)
474
+
475
+ # If it's a failure question without context requirement, penalize it
476
+ if is_failure and not requires_ctx:
477
+ priority = priority + 0.5
478
+
479
+ return priority
480
+
481
+ return sorted(questions, key=sort_key)
482
+
483
+ def _mark_optional_questions(
484
+ self, questions: List[Dict[str, Any]], total_count: int
485
+ ) -> List[Dict[str, Any]]:
486
+ """
487
+ Mark questions as optional for passed operations.
488
+
489
+ Marks lower-priority questions as optional.
490
+
491
+ Args:
492
+ questions: List of selected questions
493
+ total_count: Total number of selected questions
494
+
495
+ Returns:
496
+ Questions with optional flag set
497
+ """
498
+ # For passed operations with 5+ questions:
499
+ # - 2-3 essential (priority 1-2)
500
+ # - 3-5 optional (priority 3+)
501
+
502
+ result = []
503
+ essential_count = min(3, max(2, total_count - 4))
504
+
505
+ for i, question in enumerate(questions):
506
+ question_copy = question.copy()
507
+ if i >= essential_count:
508
+ question_copy['optional'] = True
509
+ else:
510
+ question_copy['optional'] = False
511
+ result.append(question_copy)
512
+
513
+ return result
514
+
515
+ def _build_selection_rationale(
516
+ self,
517
+ operation_type: str,
518
+ success_status: str,
519
+ base_count: int,
520
+ final_count: int,
521
+ modifiers_applied: List[str],
522
+ is_first_time: bool,
523
+ is_repeat_user: bool,
524
+ is_rapid_mode: bool,
525
+ has_errors: bool,
526
+ has_performance_outlier: bool,
527
+ ) -> str:
528
+ """
529
+ Build a clear explanation of why these questions were selected.
530
+
531
+ Args:
532
+ operation_type: Operation type
533
+ success_status: Success status
534
+ base_count: Base question count
535
+ final_count: Final question count
536
+ modifiers_applied: List of modifiers that were applied
537
+ is_first_time: Whether first-time user
538
+ is_repeat_user: Whether repeat user
539
+ is_rapid_mode: Whether in rapid mode
540
+ has_errors: Whether operation had errors
541
+ has_performance_outlier: Whether performance was outlier
542
+
543
+ Returns:
544
+ str: Human-readable rationale
545
+ """
546
+ parts = []
547
+
548
+ # Base selection
549
+ parts.append(
550
+ f"Selected {final_count} questions for {operation_type} operation "
551
+ f"with {success_status} status"
552
+ )
553
+
554
+ # User history context
555
+ if is_first_time:
556
+ parts.append("increased for first-time user")
557
+ elif is_repeat_user:
558
+ parts.append("reduced for repeat user (3+ previous operations)")
559
+
560
+ # Error context
561
+ if has_errors:
562
+ parts.append("(includes investigation questions due to errors)")
563
+
564
+ # Performance context
565
+ if has_performance_outlier:
566
+ parts.append("(includes performance investigation questions)")
567
+
568
+ # Rapid mode context
569
+ if is_rapid_mode:
570
+ parts.append("(reduced due to rapid operation pace)")
571
+
572
+ # Modifiers summary
573
+ if modifiers_applied:
574
+ modifiers_str = ', '.join(modifiers_applied)
575
+ parts.append(f"Modifiers applied: {modifiers_str}")
576
+
577
+ rationale = '. '.join(parts)
578
+ return rationale
579
+
580
+
581
+ __all__ = ['AdaptiveQuestioningEngine']