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,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']
|