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,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feedback indexer module for DevForgeAI.
|
|
3
|
+
|
|
4
|
+
Scans all feedback data sources in devforgeai/feedback/ and builds
|
|
5
|
+
a unified index at devforgeai/feedback/index.json.
|
|
6
|
+
|
|
7
|
+
Uses Python stdlib only: json, pathlib, os, re, datetime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Files at the root of devforgeai/feedback/ that are NOT feedback data
|
|
18
|
+
_EXCLUDED_ROOT_FILES = {
|
|
19
|
+
"USER-GUIDE.md",
|
|
20
|
+
"MAINTAINER-GUIDE.md",
|
|
21
|
+
"GRACEFUL-DEGRADATION.md",
|
|
22
|
+
"RETENTION-POLICY.md",
|
|
23
|
+
"IMPLEMENTATION-COMPLETE.md",
|
|
24
|
+
"QUESTION-BANK-COMPLETION-SUMMARY.md",
|
|
25
|
+
"questions.md",
|
|
26
|
+
"config.yaml",
|
|
27
|
+
"schema.json",
|
|
28
|
+
"questions.yaml",
|
|
29
|
+
"question-defaults.yaml",
|
|
30
|
+
"index.json",
|
|
31
|
+
"feedback-index.json",
|
|
32
|
+
"feedback-register.md",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Directories under devforgeai/feedback/ that are NOT feedback data
|
|
36
|
+
_EXCLUDED_DIRS = {"question-bank", "logs", "imported"}
|
|
37
|
+
|
|
38
|
+
# Subdirectories under ai-analysis/ to exclude
|
|
39
|
+
_EXCLUDED_AI_ANALYSIS_SUBDIRS = {"aggregated", "imports"}
|
|
40
|
+
|
|
41
|
+
# Regex pattern to identify artifact folder names (STORY-NNN, EPIC-NNN, RCA-NNN)
|
|
42
|
+
_ARTIFACT_PATTERN = re.compile(r"^(STORY|EPIC|RCA)-\d+$")
|
|
43
|
+
|
|
44
|
+
# Regex patterns for root-level report filenames
|
|
45
|
+
_REPORT_PATTERNS = [
|
|
46
|
+
re.compile(r"^code-review-.*\.md$"),
|
|
47
|
+
re.compile(r"^integration-test-report-.*\.md$"),
|
|
48
|
+
re.compile(r".*-coverage-analysis\.md$"),
|
|
49
|
+
re.compile(r".*-dev-complete-summary\.md$"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Regex to extract story ID from filenames
|
|
53
|
+
_STORY_ID_PATTERN = re.compile(r"(STORY-\d+)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _file_mtime_iso(filepath: Path) -> str:
|
|
57
|
+
"""Get file modification time as ISO 8601 string in UTC."""
|
|
58
|
+
mtime = os.path.getmtime(str(filepath))
|
|
59
|
+
dt = datetime.datetime.fromtimestamp(mtime, tz=datetime.timezone.utc)
|
|
60
|
+
return dt.isoformat()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_timestamp_from_json(data: dict) -> str:
|
|
64
|
+
"""Extract timestamp from JSON data, checking multiple field locations."""
|
|
65
|
+
# Direct timestamp field
|
|
66
|
+
if "timestamp" in data and isinstance(data["timestamp"], str):
|
|
67
|
+
return data["timestamp"]
|
|
68
|
+
# analysis_date field
|
|
69
|
+
if "analysis_date" in data and isinstance(data["analysis_date"], str):
|
|
70
|
+
return data["analysis_date"]
|
|
71
|
+
# Nested ai_analysis.timestamp
|
|
72
|
+
if "ai_analysis" in data and isinstance(data["ai_analysis"], dict):
|
|
73
|
+
nested = data["ai_analysis"]
|
|
74
|
+
if "timestamp" in nested and isinstance(nested["timestamp"], str):
|
|
75
|
+
return nested["timestamp"]
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_report_filename(filename: str) -> bool:
|
|
80
|
+
"""Check if a filename matches root-level report patterns."""
|
|
81
|
+
for pattern in _REPORT_PATTERNS:
|
|
82
|
+
if pattern.match(filename):
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _scan_ai_analysis(feedback_dir: Path, entries: list, errors: list) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Scan ai-analysis directory for JSON and MD files in artifact folders.
|
|
90
|
+
|
|
91
|
+
Returns count of files processed (including errors).
|
|
92
|
+
"""
|
|
93
|
+
ai_dir = feedback_dir / "ai-analysis"
|
|
94
|
+
if not ai_dir.is_dir():
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
count = 0
|
|
98
|
+
for artifact_dir in sorted(ai_dir.iterdir()):
|
|
99
|
+
if not artifact_dir.is_dir():
|
|
100
|
+
continue
|
|
101
|
+
# Skip excluded subdirectories
|
|
102
|
+
if artifact_dir.name in _EXCLUDED_AI_ANALYSIS_SUBDIRS:
|
|
103
|
+
continue
|
|
104
|
+
# Only process artifact folders matching STORY-NNN, EPIC-NNN, RCA-NNN
|
|
105
|
+
if not _ARTIFACT_PATTERN.match(artifact_dir.name):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
artifact_id = artifact_dir.name
|
|
109
|
+
|
|
110
|
+
# Walk all files recursively in this artifact folder
|
|
111
|
+
for root, _dirs, files in os.walk(str(artifact_dir)):
|
|
112
|
+
root_path = Path(root)
|
|
113
|
+
for filename in sorted(files):
|
|
114
|
+
filepath = root_path / filename
|
|
115
|
+
|
|
116
|
+
# Skip index.json at any level within ai-analysis
|
|
117
|
+
if filename == "index.json":
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if filename.endswith(".json"):
|
|
121
|
+
count += 1
|
|
122
|
+
try:
|
|
123
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
124
|
+
data = json.load(f)
|
|
125
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
126
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
127
|
+
errors.append(
|
|
128
|
+
f"Failed to parse: {rel_path}: {type(exc).__name__}"
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
timestamp = _extract_timestamp_from_json(data)
|
|
133
|
+
if not timestamp:
|
|
134
|
+
timestamp = _file_mtime_iso(filepath)
|
|
135
|
+
|
|
136
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
137
|
+
stem = filepath.stem
|
|
138
|
+
entry_id = f"{artifact_id}-{stem}"
|
|
139
|
+
|
|
140
|
+
entries.append({
|
|
141
|
+
"id": entry_id,
|
|
142
|
+
"timestamp": timestamp,
|
|
143
|
+
"source_type": "ai-analysis",
|
|
144
|
+
"story-id": artifact_id,
|
|
145
|
+
"file-path": str(rel_path).replace("\\", "/"),
|
|
146
|
+
"tags": ["ai-analysis"],
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
elif filename.endswith(".md"):
|
|
150
|
+
count += 1
|
|
151
|
+
timestamp = _file_mtime_iso(filepath)
|
|
152
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
153
|
+
stem = filepath.stem
|
|
154
|
+
entry_id = f"{artifact_id}-{stem}"
|
|
155
|
+
|
|
156
|
+
entries.append({
|
|
157
|
+
"id": entry_id,
|
|
158
|
+
"timestamp": timestamp,
|
|
159
|
+
"source_type": "ai-analysis",
|
|
160
|
+
"story-id": artifact_id,
|
|
161
|
+
"file-path": str(rel_path).replace("\\", "/"),
|
|
162
|
+
"tags": ["ai-analysis"],
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return count
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _scan_code_reviews(feedback_dir: Path, entries: list, errors: list) -> int:
|
|
169
|
+
"""
|
|
170
|
+
Scan code-review/ and code-reviews/ directories for markdown files.
|
|
171
|
+
|
|
172
|
+
Returns count of files processed.
|
|
173
|
+
"""
|
|
174
|
+
count = 0
|
|
175
|
+
for dir_name in ("code-review", "code-reviews"):
|
|
176
|
+
cr_dir = feedback_dir / dir_name
|
|
177
|
+
if not cr_dir.is_dir():
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
for filepath in sorted(cr_dir.iterdir()):
|
|
181
|
+
if not filepath.is_file():
|
|
182
|
+
continue
|
|
183
|
+
if not filepath.name.endswith(".md"):
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
count += 1
|
|
187
|
+
timestamp = _file_mtime_iso(filepath)
|
|
188
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
189
|
+
stem = filepath.stem
|
|
190
|
+
entry_id = f"code-review-{stem}"
|
|
191
|
+
|
|
192
|
+
# Extract story ID from filename
|
|
193
|
+
story_match = _STORY_ID_PATTERN.search(filepath.name)
|
|
194
|
+
story_id = story_match.group(1) if story_match else ""
|
|
195
|
+
|
|
196
|
+
entry = {
|
|
197
|
+
"id": entry_id,
|
|
198
|
+
"timestamp": timestamp,
|
|
199
|
+
"source_type": "code-review",
|
|
200
|
+
"file-path": str(rel_path).replace("\\", "/"),
|
|
201
|
+
"tags": ["code-review"],
|
|
202
|
+
}
|
|
203
|
+
if story_id:
|
|
204
|
+
entry["story-id"] = story_id
|
|
205
|
+
|
|
206
|
+
entries.append(entry)
|
|
207
|
+
|
|
208
|
+
return count
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _scan_root_reports(feedback_dir: Path, entries: list, errors: list) -> int:
|
|
212
|
+
"""
|
|
213
|
+
Scan root-level feedback directory for report files.
|
|
214
|
+
|
|
215
|
+
Returns count of files processed.
|
|
216
|
+
"""
|
|
217
|
+
count = 0
|
|
218
|
+
if not feedback_dir.is_dir():
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
for filepath in sorted(feedback_dir.iterdir()):
|
|
222
|
+
if not filepath.is_file():
|
|
223
|
+
continue
|
|
224
|
+
if filepath.name in _EXCLUDED_ROOT_FILES:
|
|
225
|
+
continue
|
|
226
|
+
if not filepath.name.endswith(".md"):
|
|
227
|
+
continue
|
|
228
|
+
if not _is_report_filename(filepath.name):
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
count += 1
|
|
232
|
+
timestamp = _file_mtime_iso(filepath)
|
|
233
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
234
|
+
stem = filepath.stem
|
|
235
|
+
entry_id = f"report-{stem}"
|
|
236
|
+
|
|
237
|
+
# Extract story ID from filename
|
|
238
|
+
story_match = _STORY_ID_PATTERN.search(filepath.name)
|
|
239
|
+
story_id = story_match.group(1) if story_match else ""
|
|
240
|
+
|
|
241
|
+
entry = {
|
|
242
|
+
"id": entry_id,
|
|
243
|
+
"timestamp": timestamp,
|
|
244
|
+
"source_type": "report",
|
|
245
|
+
"file-path": str(rel_path).replace("\\", "/"),
|
|
246
|
+
"tags": ["report"],
|
|
247
|
+
}
|
|
248
|
+
if story_id:
|
|
249
|
+
entry["story-id"] = story_id
|
|
250
|
+
|
|
251
|
+
entries.append(entry)
|
|
252
|
+
|
|
253
|
+
return count
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _scan_sessions(feedback_dir: Path, entries: list, errors: list) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Scan sessions/ directory for markdown files (backward compatibility).
|
|
259
|
+
|
|
260
|
+
Returns count of files processed. Silently skips if directory doesn't exist.
|
|
261
|
+
"""
|
|
262
|
+
sessions_dir = feedback_dir / "sessions"
|
|
263
|
+
if not sessions_dir.is_dir():
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
count = 0
|
|
267
|
+
for filepath in sorted(sessions_dir.iterdir()):
|
|
268
|
+
if not filepath.is_file():
|
|
269
|
+
continue
|
|
270
|
+
if not filepath.name.endswith(".md"):
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
count += 1
|
|
274
|
+
timestamp = _file_mtime_iso(filepath)
|
|
275
|
+
rel_path = filepath.relative_to(feedback_dir)
|
|
276
|
+
stem = filepath.stem
|
|
277
|
+
entry_id = f"session-{stem}"
|
|
278
|
+
|
|
279
|
+
entries.append({
|
|
280
|
+
"id": entry_id,
|
|
281
|
+
"timestamp": timestamp,
|
|
282
|
+
"source_type": "session",
|
|
283
|
+
"file-path": str(rel_path).replace("\\", "/"),
|
|
284
|
+
"tags": ["session"],
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return count
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def reindex_all_feedback(project_root: str, output_format: str = "text") -> int:
|
|
291
|
+
"""
|
|
292
|
+
Scan all feedback sources and build unified index.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
project_root: Path to project root directory.
|
|
296
|
+
output_format: 'text' or 'json'.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Exit code: 0 for success, 1 for error.
|
|
300
|
+
|
|
301
|
+
Side effects:
|
|
302
|
+
- Writes devforgeai/feedback/index.json
|
|
303
|
+
- Prints results to stdout
|
|
304
|
+
"""
|
|
305
|
+
root = Path(project_root)
|
|
306
|
+
feedback_dir = root / "devforgeai" / "feedback"
|
|
307
|
+
|
|
308
|
+
if not feedback_dir.is_dir():
|
|
309
|
+
if output_format == "json":
|
|
310
|
+
print(json.dumps({
|
|
311
|
+
"status": "error",
|
|
312
|
+
"message": f"Feedback directory not found: {feedback_dir}",
|
|
313
|
+
}))
|
|
314
|
+
else:
|
|
315
|
+
print(f"Error: Feedback directory not found: {feedback_dir}")
|
|
316
|
+
return 1
|
|
317
|
+
|
|
318
|
+
entries: list = []
|
|
319
|
+
errors: list = []
|
|
320
|
+
|
|
321
|
+
# Scan all sources
|
|
322
|
+
ai_count = _scan_ai_analysis(feedback_dir, entries, errors)
|
|
323
|
+
cr_count = _scan_code_reviews(feedback_dir, entries, errors)
|
|
324
|
+
report_count = _scan_root_reports(feedback_dir, entries, errors)
|
|
325
|
+
session_count = _scan_sessions(feedback_dir, entries, errors)
|
|
326
|
+
|
|
327
|
+
# Sort entries by timestamp (newest first)
|
|
328
|
+
entries.sort(key=lambda e: e.get("timestamp", ""), reverse=True)
|
|
329
|
+
|
|
330
|
+
# Build source summary
|
|
331
|
+
source_summary = {
|
|
332
|
+
"ai-analysis": sum(1 for e in entries if e["source_type"] == "ai-analysis"),
|
|
333
|
+
"session": sum(1 for e in entries if e["source_type"] == "session"),
|
|
334
|
+
"code-review": sum(1 for e in entries if e["source_type"] == "code-review"),
|
|
335
|
+
"report": sum(1 for e in entries if e["source_type"] == "report"),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
total_files = ai_count + cr_count + report_count + session_count
|
|
339
|
+
indexed_count = len(entries)
|
|
340
|
+
error_count = len(errors)
|
|
341
|
+
|
|
342
|
+
# Build index
|
|
343
|
+
now_utc = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
|
|
344
|
+
index = {
|
|
345
|
+
"version": "2.0",
|
|
346
|
+
"last-updated": now_utc,
|
|
347
|
+
"feedback-sessions": entries,
|
|
348
|
+
"source_summary": source_summary,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Write index file
|
|
352
|
+
index_path = feedback_dir / "index.json"
|
|
353
|
+
try:
|
|
354
|
+
with open(index_path, "w", encoding="utf-8") as f:
|
|
355
|
+
json.dump(index, f, indent=2)
|
|
356
|
+
except OSError as exc:
|
|
357
|
+
if output_format == "json":
|
|
358
|
+
print(json.dumps({
|
|
359
|
+
"status": "error",
|
|
360
|
+
"message": f"Failed to write index: {exc}",
|
|
361
|
+
}))
|
|
362
|
+
else:
|
|
363
|
+
print(f"Error: Failed to write index: {exc}")
|
|
364
|
+
return 1
|
|
365
|
+
|
|
366
|
+
# Print results
|
|
367
|
+
if output_format == "json":
|
|
368
|
+
result = {
|
|
369
|
+
"status": "success",
|
|
370
|
+
"total_files": total_files,
|
|
371
|
+
"indexed_count": indexed_count,
|
|
372
|
+
"error_count": error_count,
|
|
373
|
+
"sources_scanned": source_summary,
|
|
374
|
+
"errors": errors,
|
|
375
|
+
"version": "2.0",
|
|
376
|
+
}
|
|
377
|
+
print(json.dumps(result))
|
|
378
|
+
else:
|
|
379
|
+
print("\u2705 Reindex completed successfully")
|
|
380
|
+
print()
|
|
381
|
+
print(f"Total files processed: {total_files}")
|
|
382
|
+
print(f"Successfully indexed: {indexed_count}")
|
|
383
|
+
print(f"Errors encountered: {error_count}")
|
|
384
|
+
print()
|
|
385
|
+
print("Sources scanned:")
|
|
386
|
+
print(f" AI analysis files: {source_summary['ai-analysis']}")
|
|
387
|
+
print(f" Code review files: {source_summary['code-review']}")
|
|
388
|
+
print(f" Root report files: {source_summary['report']}")
|
|
389
|
+
print(f" Session files: {source_summary['session']}")
|
|
390
|
+
print()
|
|
391
|
+
print(f"Index file: devforgeai/feedback/index.json")
|
|
392
|
+
print(f"Index version: 2.0")
|
|
393
|
+
|
|
394
|
+
return 0
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hot-reload system for feedback configuration.
|
|
3
|
+
|
|
4
|
+
This module provides file watching and automatic configuration reloading
|
|
5
|
+
when the configuration file changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Callable, Any, NamedTuple
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileInfo(NamedTuple):
|
|
17
|
+
"""File information (modification time and size)."""
|
|
18
|
+
mtime: Optional[float]
|
|
19
|
+
size: Optional[int]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigFileWatcher:
|
|
23
|
+
"""Watches configuration file for changes and triggers reloads.
|
|
24
|
+
|
|
25
|
+
Monitors devforgeai/config/feedback.yaml for changes and calls
|
|
26
|
+
a callback function when modifications are detected.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
config_file: Path,
|
|
32
|
+
on_change_callback: Callable[[Path], None],
|
|
33
|
+
poll_interval: float = 0.5,
|
|
34
|
+
detection_timeout: float = 5.0
|
|
35
|
+
):
|
|
36
|
+
"""Initialize the file watcher.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config_file: Path to the configuration file to watch.
|
|
40
|
+
on_change_callback: Function to call when file changes.
|
|
41
|
+
Called with Path to the changed file.
|
|
42
|
+
poll_interval: How often to check for changes (seconds).
|
|
43
|
+
detection_timeout: Maximum time to detect change (seconds).
|
|
44
|
+
"""
|
|
45
|
+
self.config_file = config_file
|
|
46
|
+
self.on_change_callback = on_change_callback
|
|
47
|
+
self.poll_interval = poll_interval
|
|
48
|
+
self.detection_timeout = detection_timeout
|
|
49
|
+
self._last_file_info: Optional[FileInfo] = None
|
|
50
|
+
self._watch_thread: Optional[threading.Thread] = None
|
|
51
|
+
self._stop_event = threading.Event()
|
|
52
|
+
self._is_running = False
|
|
53
|
+
self._lock = threading.Lock()
|
|
54
|
+
|
|
55
|
+
def _get_file_info(self) -> FileInfo:
|
|
56
|
+
"""Get file modification time and size.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
FileInfo with mtime and size, or (None, None) if file doesn't exist.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
if self.config_file.exists():
|
|
63
|
+
stat = self.config_file.stat()
|
|
64
|
+
return FileInfo(stat.st_mtime, stat.st_size)
|
|
65
|
+
except (OSError, IOError):
|
|
66
|
+
pass
|
|
67
|
+
return FileInfo(None, None)
|
|
68
|
+
|
|
69
|
+
def _watch_loop(self) -> None:
|
|
70
|
+
"""Main file watching loop (runs in thread)."""
|
|
71
|
+
# Initialize baseline
|
|
72
|
+
self._last_file_info = self._get_file_info()
|
|
73
|
+
|
|
74
|
+
while not self._stop_event.is_set():
|
|
75
|
+
try:
|
|
76
|
+
time.sleep(self.poll_interval)
|
|
77
|
+
|
|
78
|
+
current_file_info = self._get_file_info()
|
|
79
|
+
|
|
80
|
+
# Check if file changed
|
|
81
|
+
if self._has_file_changed(current_file_info):
|
|
82
|
+
# File changed - trigger callback
|
|
83
|
+
try:
|
|
84
|
+
self.on_change_callback(self.config_file)
|
|
85
|
+
except Exception:
|
|
86
|
+
# Silently ignore callback errors
|
|
87
|
+
pass
|
|
88
|
+
self._last_file_info = current_file_info
|
|
89
|
+
|
|
90
|
+
except Exception:
|
|
91
|
+
# Silently ignore watch loop errors
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def _has_file_changed(self, current_info: FileInfo) -> bool:
|
|
95
|
+
"""Check if file information has changed.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
current_info: Current file information.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if file has changed, False otherwise.
|
|
102
|
+
"""
|
|
103
|
+
if self._last_file_info is None:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
return (current_info.mtime is not None and
|
|
107
|
+
self._last_file_info.mtime is not None and
|
|
108
|
+
(current_info.mtime != self._last_file_info.mtime or
|
|
109
|
+
current_info.size != self._last_file_info.size))
|
|
110
|
+
|
|
111
|
+
def start(self) -> None:
|
|
112
|
+
"""Start watching the configuration file."""
|
|
113
|
+
with self._lock:
|
|
114
|
+
if self._is_running:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
self._stop_event.clear()
|
|
118
|
+
self._watch_thread = threading.Thread(target=self._watch_loop, daemon=True)
|
|
119
|
+
self._watch_thread.start()
|
|
120
|
+
self._is_running = True
|
|
121
|
+
|
|
122
|
+
def stop(self) -> None:
|
|
123
|
+
"""Stop watching the configuration file."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
if not self._is_running:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
self._stop_event.set()
|
|
129
|
+
if self._watch_thread and self._watch_thread.is_alive():
|
|
130
|
+
self._watch_thread.join(timeout=1.0)
|
|
131
|
+
self._is_running = False
|
|
132
|
+
|
|
133
|
+
def is_running(self) -> bool:
|
|
134
|
+
"""Check if watcher is currently running.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if watcher is active, False otherwise.
|
|
138
|
+
"""
|
|
139
|
+
with self._lock:
|
|
140
|
+
return self._is_running
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class HotReloadManager:
|
|
144
|
+
"""Manages configuration hot-reload lifecycle.
|
|
145
|
+
|
|
146
|
+
Coordinates file watching and configuration updates.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
config_file: Path,
|
|
152
|
+
load_config_callback: Callable[[], Any]
|
|
153
|
+
):
|
|
154
|
+
"""Initialize hot-reload manager.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
config_file: Path to the configuration file.
|
|
158
|
+
load_config_callback: Function that loads and returns new configuration.
|
|
159
|
+
Called when file changes are detected.
|
|
160
|
+
"""
|
|
161
|
+
self.config_file = config_file
|
|
162
|
+
self.load_config_callback = load_config_callback
|
|
163
|
+
self._watcher: Optional[ConfigFileWatcher] = None
|
|
164
|
+
self._lock = threading.Lock()
|
|
165
|
+
self._current_config: Optional[Any] = None
|
|
166
|
+
|
|
167
|
+
def _on_config_change(self, changed_file: Path) -> None:
|
|
168
|
+
"""Handle configuration file change.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
changed_file: Path to the changed file.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
with self._lock:
|
|
175
|
+
# Load new configuration
|
|
176
|
+
new_config = self.load_config_callback()
|
|
177
|
+
self._current_config = new_config
|
|
178
|
+
except Exception:
|
|
179
|
+
# Keep previous valid configuration on error
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
def start(self) -> None:
|
|
183
|
+
"""Start the hot-reload manager."""
|
|
184
|
+
with self._lock:
|
|
185
|
+
if self._watcher is not None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
self._watcher = ConfigFileWatcher(
|
|
189
|
+
self.config_file,
|
|
190
|
+
self._on_config_change
|
|
191
|
+
)
|
|
192
|
+
self._watcher.start()
|
|
193
|
+
|
|
194
|
+
def stop(self) -> None:
|
|
195
|
+
"""Stop the hot-reload manager."""
|
|
196
|
+
with self._lock:
|
|
197
|
+
if self._watcher is not None:
|
|
198
|
+
self._watcher.stop()
|
|
199
|
+
self._watcher = None
|
|
200
|
+
|
|
201
|
+
def is_running(self) -> bool:
|
|
202
|
+
"""Check if hot-reload is active.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if hot-reload manager is running, False otherwise.
|
|
206
|
+
"""
|
|
207
|
+
with self._lock:
|
|
208
|
+
return self._watcher is not None and self._watcher.is_running()
|
|
209
|
+
|
|
210
|
+
def get_current_config(self) -> Optional[Any]:
|
|
211
|
+
"""Get the current loaded configuration.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Current configuration object or None if not loaded.
|
|
215
|
+
"""
|
|
216
|
+
with self._lock:
|
|
217
|
+
return self._current_config
|
|
218
|
+
|
|
219
|
+
def set_current_config(self, config: Any) -> None:
|
|
220
|
+
"""Set the current configuration (used during initialization).
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
config: Configuration object to set as current.
|
|
224
|
+
"""
|
|
225
|
+
with self._lock:
|
|
226
|
+
self._current_config = config
|