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.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. 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