ref-agents 1.0.0__py3-none-any.whl
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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Dependency Graph Generator for REF Agents.
|
|
2
|
+
|
|
3
|
+
Scans REQ.md files, extracts depends_on/blocks fields,
|
|
4
|
+
and generates Mermaid diagrams + JSON for sequencing engine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
# Regex patterns for REQ.md parsing
|
|
17
|
+
STORY_ID_PATTERN = re.compile(r"#\s*Requirement:\s*\[([A-Z]+-\d+)\]")
|
|
18
|
+
STATUS_PATTERN = re.compile(r"\*\*Status:\*\*\s*(\w+(?:\s+\w+)?)")
|
|
19
|
+
DEPENDS_ON_PATTERN = re.compile(r"\|\s*Depends On\s*\|\s*([A-Z]+-\d+)\s*\|")
|
|
20
|
+
BLOCKS_PATTERN = re.compile(r"\|\s*Blocks\s*\|\s*([A-Z]+-\d+)\s*\|")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class StoryNode:
|
|
25
|
+
"""Represents a story in the dependency graph."""
|
|
26
|
+
|
|
27
|
+
story_id: str
|
|
28
|
+
status: str = "Draft"
|
|
29
|
+
depends_on: list[str] = field(default_factory=list)
|
|
30
|
+
blocks: list[str] = field(default_factory=list)
|
|
31
|
+
file_path: str = ""
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict[str, str | list[str]]:
|
|
34
|
+
"""Convert to dictionary for JSON serialization."""
|
|
35
|
+
return {
|
|
36
|
+
"story_id": self.story_id,
|
|
37
|
+
"status": self.status,
|
|
38
|
+
"depends_on": self.depends_on,
|
|
39
|
+
"blocks": self.blocks,
|
|
40
|
+
"file_path": self.file_path,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DependencyGraph:
|
|
46
|
+
"""Dependency graph containing all story nodes."""
|
|
47
|
+
|
|
48
|
+
nodes: dict[str, StoryNode] = field(default_factory=dict)
|
|
49
|
+
errors: list[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def add_node(self, node: StoryNode) -> None:
|
|
52
|
+
"""Add a story node to the graph."""
|
|
53
|
+
self.nodes[node.story_id] = node
|
|
54
|
+
|
|
55
|
+
def get_node(self, story_id: str) -> StoryNode | None:
|
|
56
|
+
"""Get a node by story ID."""
|
|
57
|
+
return self.nodes.get(story_id)
|
|
58
|
+
|
|
59
|
+
def to_json(self) -> str:
|
|
60
|
+
"""Serialize graph to JSON."""
|
|
61
|
+
data = {
|
|
62
|
+
"nodes": {sid: node.to_dict() for sid, node in self.nodes.items()},
|
|
63
|
+
"edges": self._get_edges(),
|
|
64
|
+
}
|
|
65
|
+
return json.dumps(data, indent=2)
|
|
66
|
+
|
|
67
|
+
def _get_edges(self) -> list[dict[str, str]]:
|
|
68
|
+
"""Extract all dependency edges."""
|
|
69
|
+
edges = []
|
|
70
|
+
for node in self.nodes.values():
|
|
71
|
+
for dep in node.depends_on:
|
|
72
|
+
edges.append({"from": dep, "to": node.story_id, "type": "depends_on"})
|
|
73
|
+
for blocked in node.blocks:
|
|
74
|
+
edges.append({"from": node.story_id, "to": blocked, "type": "blocks"})
|
|
75
|
+
return edges
|
|
76
|
+
|
|
77
|
+
def to_mermaid(self) -> str:
|
|
78
|
+
"""Generate Mermaid diagram."""
|
|
79
|
+
lines = ["graph LR"]
|
|
80
|
+
|
|
81
|
+
for node in self.nodes.values():
|
|
82
|
+
style = _get_status_style(node.status)
|
|
83
|
+
lines.append(f" {node.story_id}{style}")
|
|
84
|
+
|
|
85
|
+
for node in self.nodes.values():
|
|
86
|
+
for dep in node.depends_on:
|
|
87
|
+
if dep in self.nodes:
|
|
88
|
+
lines.append(f" {dep} --> {node.story_id}")
|
|
89
|
+
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_status_style(status: str) -> str:
|
|
94
|
+
"""Get Mermaid node style based on status."""
|
|
95
|
+
status_lower = status.lower()
|
|
96
|
+
if status_lower == "done":
|
|
97
|
+
return ":::done"
|
|
98
|
+
if status_lower in ("in progress", "in development"):
|
|
99
|
+
return ":::inprogress"
|
|
100
|
+
if status_lower == "blocked":
|
|
101
|
+
return ":::blocked"
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_req_file(file_path: Path) -> StoryNode | None:
|
|
106
|
+
"""Parse a REQ.md file and extract story metadata.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Path to REQ.md file.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
StoryNode if valid, None if parsing fails.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
content = file_path.read_text(encoding="utf-8")
|
|
116
|
+
except OSError as e:
|
|
117
|
+
logger.warning("req_file_read_error", file=str(file_path), error=str(e))
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# Extract story ID
|
|
121
|
+
story_match = STORY_ID_PATTERN.search(content)
|
|
122
|
+
if not story_match:
|
|
123
|
+
logger.warning(
|
|
124
|
+
"no_story_id_found",
|
|
125
|
+
file=str(file_path),
|
|
126
|
+
expected_format="# Requirement: [STORY-001] Title",
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
story_id = story_match.group(1)
|
|
131
|
+
|
|
132
|
+
# Extract status
|
|
133
|
+
status_match = STATUS_PATTERN.search(content)
|
|
134
|
+
status = status_match.group(1) if status_match else "Draft"
|
|
135
|
+
|
|
136
|
+
# Extract dependencies
|
|
137
|
+
depends_on = DEPENDS_ON_PATTERN.findall(content)
|
|
138
|
+
blocks = BLOCKS_PATTERN.findall(content)
|
|
139
|
+
|
|
140
|
+
return StoryNode(
|
|
141
|
+
story_id=story_id,
|
|
142
|
+
status=status,
|
|
143
|
+
depends_on=depends_on,
|
|
144
|
+
blocks=blocks,
|
|
145
|
+
file_path=str(file_path),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def scan_requirements_directory(directory: str | Path) -> DependencyGraph:
|
|
150
|
+
"""Scan directory for REQ.md files and build dependency graph.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
directory: Path to requirements directory.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
DependencyGraph with all stories and dependencies.
|
|
157
|
+
"""
|
|
158
|
+
graph = DependencyGraph()
|
|
159
|
+
dir_path = Path(directory)
|
|
160
|
+
|
|
161
|
+
if not dir_path.exists():
|
|
162
|
+
graph.errors.append(f"Directory not found: {directory}")
|
|
163
|
+
return graph
|
|
164
|
+
|
|
165
|
+
# Find all requirement files: REQ-*.md, req-*.md, STORY-*.md, EPIC-*.md
|
|
166
|
+
req_files = (
|
|
167
|
+
list(dir_path.rglob("*REQ*.md"))
|
|
168
|
+
+ list(dir_path.rglob("*req*.md"))
|
|
169
|
+
+ list(dir_path.rglob("STORY-*.md"))
|
|
170
|
+
+ list(dir_path.rglob("EPIC-*.md"))
|
|
171
|
+
)
|
|
172
|
+
req_files = list(set(req_files)) # Deduplicate
|
|
173
|
+
|
|
174
|
+
for req_file in req_files:
|
|
175
|
+
node = parse_req_file(req_file)
|
|
176
|
+
if node:
|
|
177
|
+
graph.add_node(node)
|
|
178
|
+
else:
|
|
179
|
+
graph.errors.append(
|
|
180
|
+
f"Skipped {req_file.name} — no story ID found. "
|
|
181
|
+
f"Expected heading: `# Requirement: [STORY-001] Title`"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
logger.info(
|
|
185
|
+
"dependency_scan_complete",
|
|
186
|
+
files_scanned=len(req_files),
|
|
187
|
+
stories_found=len(graph.nodes),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return graph
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def generate_dependency_graph(directory: str) -> str:
|
|
194
|
+
"""Generate dependency graph artifact.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
directory: Path to requirements directory.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Status message with artifact location.
|
|
201
|
+
"""
|
|
202
|
+
graph = scan_requirements_directory(directory)
|
|
203
|
+
|
|
204
|
+
# Fatal errors (e.g. directory not found) block output
|
|
205
|
+
fatal_errors = [e for e in graph.errors if e.startswith("Directory not found")]
|
|
206
|
+
if fatal_errors:
|
|
207
|
+
return f"Errors during scan: {fatal_errors}"
|
|
208
|
+
|
|
209
|
+
if not graph.nodes:
|
|
210
|
+
parse_hints = [e for e in graph.errors if "Skipped" in e]
|
|
211
|
+
hint_block = "\n".join(f" • {h}" for h in parse_hints)
|
|
212
|
+
return (
|
|
213
|
+
f"No parseable story files found in {directory}.\n\n"
|
|
214
|
+
f"Expected file format:\n"
|
|
215
|
+
f" # Requirement: [STORY-001] Title\n"
|
|
216
|
+
f" **Status:** Ready\n"
|
|
217
|
+
f" ## User Story\n"
|
|
218
|
+
f" As a user...\n"
|
|
219
|
+
f" ## Acceptance Criteria\n"
|
|
220
|
+
f" 1. Given...\n\n"
|
|
221
|
+
+ (
|
|
222
|
+
f"Files found but skipped:\n{hint_block}"
|
|
223
|
+
if hint_block
|
|
224
|
+
else "No .md files found."
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Generate Mermaid markdown
|
|
229
|
+
mermaid_content = f"""# Dependency Graph
|
|
230
|
+
|
|
231
|
+
*Auto-generated by dependency_graph.py*
|
|
232
|
+
|
|
233
|
+
## Story Dependencies
|
|
234
|
+
|
|
235
|
+
```mermaid
|
|
236
|
+
{graph.to_mermaid()}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Style Legend
|
|
240
|
+
|
|
241
|
+
- Default: Draft/Pending
|
|
242
|
+
- `:::done`: Completed
|
|
243
|
+
- `:::inprogress`: In Progress
|
|
244
|
+
- `:::blocked`: Blocked
|
|
245
|
+
|
|
246
|
+
## Raw Data
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{graph.to_json()}
|
|
250
|
+
```
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
# Write artifact
|
|
254
|
+
output_path = Path(directory) / "DEPENDENCY_GRAPH.md"
|
|
255
|
+
try:
|
|
256
|
+
output_path.write_text(mermaid_content, encoding="utf-8")
|
|
257
|
+
return f"Generated: `{output_path}` ({len(graph.nodes)} stories)"
|
|
258
|
+
except OSError as e:
|
|
259
|
+
return f"Failed to write artifact: {e}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_graph_json(directory: str) -> str:
|
|
263
|
+
"""Get dependency graph as JSON for sequencing engine.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
directory: Path to requirements directory.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
JSON string with graph data.
|
|
270
|
+
"""
|
|
271
|
+
graph = scan_requirements_directory(directory)
|
|
272
|
+
return graph.to_json()
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""REF Discovery Audit Tool.
|
|
2
|
+
|
|
3
|
+
Post-completion verification for discovery artifacts.
|
|
4
|
+
Checks existence, placement, and content compliance.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from ref_agents.tools.discovery_audit import audit_discovery
|
|
8
|
+
result = audit_discovery("/path/to/project")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from ref_agents.tool_names import TOOL_SCAN_HEALTH
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AuditCheck:
|
|
23
|
+
"""Single audit check result."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
passed: bool
|
|
27
|
+
message: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AuditResult:
|
|
32
|
+
"""Complete audit result."""
|
|
33
|
+
|
|
34
|
+
status: str # "PASS" or "FAIL"
|
|
35
|
+
checks: list[AuditCheck] = field(default_factory=list)
|
|
36
|
+
remediation: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def passed_count(self) -> int:
|
|
40
|
+
return sum(1 for c in self.checks if c.passed)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def failed_count(self) -> int:
|
|
44
|
+
return sum(1 for c in self.checks if not c.passed)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_artifact_existence(root: Path) -> list[AuditCheck]:
|
|
48
|
+
"""Check required artifacts exist.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
root: Project root directory.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of audit checks.
|
|
55
|
+
"""
|
|
56
|
+
checks: list[AuditCheck] = []
|
|
57
|
+
|
|
58
|
+
required = {
|
|
59
|
+
"codemap/CODE_MAP.md": "Root codemap meta-map",
|
|
60
|
+
"FEATURES.md": "Features manifest",
|
|
61
|
+
"AGENTS.md": "AI protocol file",
|
|
62
|
+
"docs/ARCHITECTURE.md": "Architecture documentation",
|
|
63
|
+
"docs/EXTERNAL_DEPS.md": "External dependencies",
|
|
64
|
+
"docs/TECH_DEBT.md": "Technical debt registry",
|
|
65
|
+
"ref-reports/context_graph.json": "Context graph for dashboard",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for path, desc in required.items():
|
|
69
|
+
full_path = root / path
|
|
70
|
+
exists = full_path.exists()
|
|
71
|
+
checks.append(
|
|
72
|
+
AuditCheck(
|
|
73
|
+
name=f"artifact_{path.replace('/', '_')}",
|
|
74
|
+
passed=exists,
|
|
75
|
+
message=f"{desc}: {'✅ Found' if exists else '❌ Missing'}",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Check codemap has package maps
|
|
80
|
+
codemap_dir = root / "codemap"
|
|
81
|
+
if codemap_dir.exists():
|
|
82
|
+
package_maps = list(codemap_dir.glob("*.md"))
|
|
83
|
+
# Exclude meta-map
|
|
84
|
+
package_maps = [p for p in package_maps if p.name != "CODE_MAP.md"]
|
|
85
|
+
has_package_maps = len(package_maps) > 0
|
|
86
|
+
checks.append(
|
|
87
|
+
AuditCheck(
|
|
88
|
+
name="codemap_package_maps",
|
|
89
|
+
passed=has_package_maps,
|
|
90
|
+
message=f"Package codemaps: {'✅ ' + str(len(package_maps)) + ' found' if has_package_maps else '❌ None found'}",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return checks
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _check_placement_validation(root: Path) -> list[AuditCheck]:
|
|
98
|
+
"""Check artifacts are in correct locations.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
root: Project root directory.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of audit checks.
|
|
105
|
+
"""
|
|
106
|
+
checks: list[AuditCheck] = []
|
|
107
|
+
|
|
108
|
+
# Should NOT be at root
|
|
109
|
+
misplaced_at_root = [
|
|
110
|
+
("ARCHITECTURE.md", "docs/ARCHITECTURE.md"),
|
|
111
|
+
("EXTERNAL_DEPS.md", "docs/EXTERNAL_DEPS.md"),
|
|
112
|
+
("EXTERNAL_APIS.md", "docs/EXTERNAL_DEPS.md"),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
for src, expected in misplaced_at_root:
|
|
116
|
+
src_path = root / src
|
|
117
|
+
if src_path.exists():
|
|
118
|
+
checks.append(
|
|
119
|
+
AuditCheck(
|
|
120
|
+
name=f"misplaced_{src}",
|
|
121
|
+
passed=False,
|
|
122
|
+
message=f"⚠️ `{src}` at root should be at `{expected}`",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Should NOT have unknown/ directory
|
|
127
|
+
unknown_dir = root / "ref-reports" / "unknown"
|
|
128
|
+
if unknown_dir.exists():
|
|
129
|
+
checks.append(
|
|
130
|
+
AuditCheck(
|
|
131
|
+
name="unknown_directory",
|
|
132
|
+
passed=False,
|
|
133
|
+
message="❌ `ref-reports/unknown/` exists - indicates agent routing issue",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
checks.append(
|
|
138
|
+
AuditCheck(
|
|
139
|
+
name="unknown_directory",
|
|
140
|
+
passed=True,
|
|
141
|
+
message="✅ No `unknown/` directory in ref-reports",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# CODE_MAP.md should NOT be scattered in source dirs
|
|
146
|
+
scattered_codemaps: list[Path] = []
|
|
147
|
+
for codemap in root.rglob("CODE_MAP.md"):
|
|
148
|
+
# Skip if in codemap/ directory
|
|
149
|
+
if "codemap" in codemap.parts:
|
|
150
|
+
continue
|
|
151
|
+
# Skip if in docs/
|
|
152
|
+
if "docs" in codemap.parts:
|
|
153
|
+
continue
|
|
154
|
+
# This is a scattered codemap
|
|
155
|
+
scattered_codemaps.append(codemap)
|
|
156
|
+
|
|
157
|
+
if scattered_codemaps:
|
|
158
|
+
checks.append(
|
|
159
|
+
AuditCheck(
|
|
160
|
+
name="scattered_codemaps",
|
|
161
|
+
passed=False,
|
|
162
|
+
message=f"⚠️ Found {len(scattered_codemaps)} CODE_MAP.md in source dirs (should be in codemap/)",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return checks
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _check_content_validation(root: Path) -> list[AuditCheck]:
|
|
170
|
+
"""Check artifact content meets requirements.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
root: Project root directory.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
List of audit checks.
|
|
177
|
+
"""
|
|
178
|
+
checks: list[AuditCheck] = []
|
|
179
|
+
|
|
180
|
+
# AGENTS.md must contain fix workflow
|
|
181
|
+
agents_path = root / "AGENTS.md"
|
|
182
|
+
if agents_path.exists():
|
|
183
|
+
content = agents_path.read_text(encoding="utf-8")
|
|
184
|
+
has_workflow = "forensic_engineer" in content and "strategist" in content
|
|
185
|
+
checks.append(
|
|
186
|
+
AuditCheck(
|
|
187
|
+
name="agents_fix_workflow",
|
|
188
|
+
passed=has_workflow,
|
|
189
|
+
message=f"AGENTS.md fix workflow: {'✅ Present' if has_workflow else '❌ Missing mandatory workflow'}",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# FEATURES.md must have at least 1 feature
|
|
194
|
+
features_path = root / "FEATURES.md"
|
|
195
|
+
if features_path.exists():
|
|
196
|
+
content = features_path.read_text(encoding="utf-8")
|
|
197
|
+
has_features = "|" in content and "Feature" in content
|
|
198
|
+
checks.append(
|
|
199
|
+
AuditCheck(
|
|
200
|
+
name="features_content",
|
|
201
|
+
passed=has_features,
|
|
202
|
+
message=f"FEATURES.md content: {'✅ Has features' if has_features else '❌ No features listed'}",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Codemaps must have Purpose section
|
|
207
|
+
codemap_dir = root / "codemap"
|
|
208
|
+
if codemap_dir.exists():
|
|
209
|
+
package_maps = [p for p in codemap_dir.glob("*.md") if p.name != "CODE_MAP.md"]
|
|
210
|
+
if package_maps:
|
|
211
|
+
sample = package_maps[0]
|
|
212
|
+
content = sample.read_text(encoding="utf-8")
|
|
213
|
+
has_purpose = "## Purpose" in content
|
|
214
|
+
checks.append(
|
|
215
|
+
AuditCheck(
|
|
216
|
+
name="codemap_purpose_section",
|
|
217
|
+
passed=has_purpose,
|
|
218
|
+
message=f"Codemap purpose sections: {'✅ Present' if has_purpose else '❌ Missing'}",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return checks
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _generate_remediation(result: AuditResult, root: Path) -> list[str]:
|
|
226
|
+
"""Generate remediation steps for failed checks.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
result: Audit result with checks.
|
|
230
|
+
root: Project root.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of remediation commands/steps.
|
|
234
|
+
"""
|
|
235
|
+
remediation: list[str] = []
|
|
236
|
+
|
|
237
|
+
for check in result.checks:
|
|
238
|
+
if check.passed:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if "codemap" in check.name and "Missing" in check.message:
|
|
242
|
+
remediation.append("Run: generate_codemaps()")
|
|
243
|
+
|
|
244
|
+
if "FEATURES" in check.message and "Missing" in check.message:
|
|
245
|
+
remediation.append("Run: generate_features_file()")
|
|
246
|
+
|
|
247
|
+
if "AGENTS" in check.message and "Missing" in check.message:
|
|
248
|
+
remediation.append("Run: generate_agents_file()")
|
|
249
|
+
|
|
250
|
+
if "TECH_DEBT" in check.message:
|
|
251
|
+
remediation.append(
|
|
252
|
+
f"Run: {TOOL_SCAN_HEALTH.format(directory='.')} to generate docs/TECH_DEBT.md"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if "context_graph" in check.name:
|
|
256
|
+
remediation.append("Run: populate_context_graph()")
|
|
257
|
+
|
|
258
|
+
if "misplaced" in check.name:
|
|
259
|
+
remediation.append(
|
|
260
|
+
f"Run: {TOOL_SCAN_HEALTH.format(directory='.')} to relocate artifacts"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if "unknown_directory" in check.name and not check.passed:
|
|
264
|
+
remediation.append("Delete ref-reports/unknown/ and re-run scans")
|
|
265
|
+
|
|
266
|
+
return list(set(remediation)) # Dedupe
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _check_migration_artifacts(root: Path) -> list[AuditCheck]:
|
|
270
|
+
"""Check migration-specific artifacts exist when migration mode is active.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
root: Project root directory.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of audit checks for migration artifacts.
|
|
277
|
+
"""
|
|
278
|
+
checks: list[AuditCheck] = []
|
|
279
|
+
|
|
280
|
+
md_path = root / "docs" / "MIGRATION_MAP.md"
|
|
281
|
+
md_exists = md_path.exists() and md_path.stat().st_size > 0
|
|
282
|
+
checks.append(
|
|
283
|
+
AuditCheck(
|
|
284
|
+
name="migration_map_md",
|
|
285
|
+
passed=md_exists,
|
|
286
|
+
message=f"docs/MIGRATION_MAP.md: {'✅ Found' if md_exists else '❌ Missing — required in migration mode'}",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
json_path = root / "ref-reports" / "migration_map.json"
|
|
291
|
+
json_exists = False
|
|
292
|
+
if json_path.exists() and json_path.stat().st_size > 0:
|
|
293
|
+
try:
|
|
294
|
+
import json as _json
|
|
295
|
+
|
|
296
|
+
_json.loads(json_path.read_text(encoding="utf-8"))
|
|
297
|
+
json_exists = True
|
|
298
|
+
except (ValueError, OSError):
|
|
299
|
+
json_exists = False
|
|
300
|
+
checks.append(
|
|
301
|
+
AuditCheck(
|
|
302
|
+
name="migration_map_json",
|
|
303
|
+
passed=json_exists,
|
|
304
|
+
message=f"ref-reports/migration_map.json: {'✅ Found and valid' if json_exists else '❌ Missing or invalid — required in migration mode'}",
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return checks
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def audit_discovery(directory: str) -> str:
|
|
312
|
+
"""Audit discovery artifacts for completeness and compliance.
|
|
313
|
+
|
|
314
|
+
Checks:
|
|
315
|
+
- Artifact existence (codemap/, FEATURES.md, AGENTS.md, etc.)
|
|
316
|
+
- Placement validation (no misplaced files, no unknown/)
|
|
317
|
+
- Content validation (required sections present)
|
|
318
|
+
- Migration artifacts (MIGRATION_MAP.md + migration_map.json) when migration mode active
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
directory: Path to project root.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Status message with audit results.
|
|
325
|
+
"""
|
|
326
|
+
root = Path(directory)
|
|
327
|
+
|
|
328
|
+
if not root.exists():
|
|
329
|
+
return f"Error: Directory {directory} does not exist."
|
|
330
|
+
|
|
331
|
+
result = AuditResult(status="PASS")
|
|
332
|
+
|
|
333
|
+
# Run all checks
|
|
334
|
+
result.checks.extend(_check_artifact_existence(root))
|
|
335
|
+
result.checks.extend(_check_placement_validation(root))
|
|
336
|
+
result.checks.extend(_check_content_validation(root))
|
|
337
|
+
|
|
338
|
+
# Migration artifact gate — only when migration mode active
|
|
339
|
+
from ref_agents.session import SessionManager
|
|
340
|
+
|
|
341
|
+
if SessionManager.get().is_migration_mode():
|
|
342
|
+
result.checks.extend(_check_migration_artifacts(root))
|
|
343
|
+
|
|
344
|
+
# Determine overall status
|
|
345
|
+
if result.failed_count > 0:
|
|
346
|
+
result.status = "FAIL"
|
|
347
|
+
result.remediation = _generate_remediation(result, root)
|
|
348
|
+
|
|
349
|
+
# Format output
|
|
350
|
+
status_icon = "✅" if result.status == "PASS" else "❌"
|
|
351
|
+
output = f"{status_icon} **Discovery Audit: {result.status}**\n\n"
|
|
352
|
+
output += (
|
|
353
|
+
f"**Passed:** {result.passed_count} | **Failed:** {result.failed_count}\n\n"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
output += "## Check Results\n\n"
|
|
357
|
+
for check in result.checks:
|
|
358
|
+
output += f"- {check.message}\n"
|
|
359
|
+
|
|
360
|
+
if result.remediation:
|
|
361
|
+
output += "\n## Remediation Steps\n\n"
|
|
362
|
+
for step in result.remediation:
|
|
363
|
+
output += f"1. {step}\n"
|
|
364
|
+
|
|
365
|
+
logger.info(
|
|
366
|
+
"discovery_audit_complete",
|
|
367
|
+
status=result.status,
|
|
368
|
+
passed=result.passed_count,
|
|
369
|
+
failed=result.failed_count,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return output
|