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,393 @@
|
|
|
1
|
+
"""Cross-Repo Linker for REF Agents.
|
|
2
|
+
|
|
3
|
+
Compares EXTERNAL_APIS.md registry against detected dependencies
|
|
4
|
+
to find unregistered deps and stale registry entries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
from ref_agents.tools.external_detector import scan_directory
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
# Regex patterns for parsing EXTERNAL_APIS.md tables
|
|
18
|
+
ENV_VAR_ROW = re.compile(r"\|\s*`([A-Z_]+)`\s*\|")
|
|
19
|
+
LIBRARY_ROW = re.compile(r"\|\s*`([a-z0-9_-]+)`\s*\|")
|
|
20
|
+
DOMAIN_ROW = re.compile(r"\|\s*`?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})`?\s*\|")
|
|
21
|
+
|
|
22
|
+
# Section headers
|
|
23
|
+
ENV_SECTION = re.compile(r"###?\s*Environment Variables", re.IGNORECASE)
|
|
24
|
+
LIB_SECTION = re.compile(r"###?\s*API Clients|Libraries", re.IGNORECASE)
|
|
25
|
+
URL_SECTION = re.compile(r"###?\s*External URLs", re.IGNORECASE)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RegisteredDependency:
|
|
30
|
+
"""A dependency registered in EXTERNAL_APIS.md."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
dep_type: str # env_var, api_client, url
|
|
34
|
+
purpose: str = ""
|
|
35
|
+
contract: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DependencyGap:
|
|
40
|
+
"""A gap between registry and detected dependencies."""
|
|
41
|
+
|
|
42
|
+
gap_type: str # unregistered, stale
|
|
43
|
+
dep_type: str # env_var, api_client, url
|
|
44
|
+
name: str
|
|
45
|
+
location: str = "" # file:line for unregistered
|
|
46
|
+
notes: str = ""
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, str]:
|
|
49
|
+
"""Convert to dictionary."""
|
|
50
|
+
return {
|
|
51
|
+
"gap_type": self.gap_type,
|
|
52
|
+
"dep_type": self.dep_type,
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"location": self.location,
|
|
55
|
+
"notes": self.notes,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LinkReport:
|
|
61
|
+
"""Result of cross-repo linking analysis."""
|
|
62
|
+
|
|
63
|
+
matched: list[str] = field(default_factory=list)
|
|
64
|
+
unregistered: list[DependencyGap] = field(default_factory=list)
|
|
65
|
+
stale: list[DependencyGap] = field(default_factory=list)
|
|
66
|
+
registry_count: int = 0
|
|
67
|
+
detected_count: int = 0
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def total_gaps(self) -> int:
|
|
71
|
+
"""Total number of gaps."""
|
|
72
|
+
return len(self.unregistered) + len(self.stale)
|
|
73
|
+
|
|
74
|
+
def to_markdown(self) -> str:
|
|
75
|
+
"""Generate markdown report."""
|
|
76
|
+
lines = [
|
|
77
|
+
"# External Dependency Link Report",
|
|
78
|
+
"",
|
|
79
|
+
"*Auto-generated by cross_repo_linker.py*",
|
|
80
|
+
"",
|
|
81
|
+
"## Summary",
|
|
82
|
+
"",
|
|
83
|
+
f"- Registered in EXTERNAL_APIS.md: {self.registry_count}",
|
|
84
|
+
f"- Detected in codebase: {self.detected_count}",
|
|
85
|
+
f"- Matched: {len(self.matched)}",
|
|
86
|
+
f"- **Unregistered: {len(self.unregistered)}**",
|
|
87
|
+
f"- **Stale entries: {len(self.stale)}**",
|
|
88
|
+
"",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
if not self.unregistered and not self.stale:
|
|
92
|
+
lines.append("✅ **All dependencies are registered and accounted for.**")
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
if self.unregistered:
|
|
96
|
+
lines.append("## ⚠️ Unregistered Dependencies")
|
|
97
|
+
lines.append("")
|
|
98
|
+
lines.append("These were detected in code but not in EXTERNAL_APIS.md:")
|
|
99
|
+
lines.append("")
|
|
100
|
+
lines.append("| Type | Name | Location | Action |")
|
|
101
|
+
lines.append("|------|------|----------|--------|")
|
|
102
|
+
for gap in self.unregistered:
|
|
103
|
+
lines.append(
|
|
104
|
+
f"| {gap.dep_type} | `{gap.name}` | {gap.location} | Add to registry |"
|
|
105
|
+
)
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
if self.stale:
|
|
109
|
+
lines.append("## 🗑️ Stale Registry Entries")
|
|
110
|
+
lines.append("")
|
|
111
|
+
lines.append("These are in EXTERNAL_APIS.md but not detected in code:")
|
|
112
|
+
lines.append("")
|
|
113
|
+
lines.append("| Type | Name | Action |")
|
|
114
|
+
lines.append("|------|------|--------|")
|
|
115
|
+
for gap in self.stale:
|
|
116
|
+
lines.append(
|
|
117
|
+
f"| {gap.dep_type} | `{gap.name}` | Remove or verify needed |"
|
|
118
|
+
)
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
if self.matched:
|
|
122
|
+
lines.append("## ✅ Matched Dependencies")
|
|
123
|
+
lines.append("")
|
|
124
|
+
lines.append("These are registered and detected:")
|
|
125
|
+
lines.append("")
|
|
126
|
+
for name in self.matched:
|
|
127
|
+
lines.append(f"- `{name}`")
|
|
128
|
+
lines.append("")
|
|
129
|
+
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _detect_section(line: str) -> str | None:
|
|
134
|
+
"""Detect if line marks a new section."""
|
|
135
|
+
if ENV_SECTION.search(line):
|
|
136
|
+
return "env_vars"
|
|
137
|
+
if LIB_SECTION.search(line):
|
|
138
|
+
return "api_clients"
|
|
139
|
+
if URL_SECTION.search(line):
|
|
140
|
+
return "urls"
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _extract_registry_entry(
|
|
145
|
+
line: str, current_section: str, registry: dict[str, set[str]]
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Extract entry from table line into registry."""
|
|
148
|
+
# Skip non-table lines
|
|
149
|
+
if not line.strip().startswith("|"):
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Skip header/separator rows
|
|
153
|
+
if "---" in line or "Env Var" in line or "Library" in line or "Domain" in line:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if current_section == "env_vars":
|
|
157
|
+
match = ENV_VAR_ROW.search(line)
|
|
158
|
+
if match:
|
|
159
|
+
registry["env_vars"].add(match.group(1))
|
|
160
|
+
elif current_section == "api_clients":
|
|
161
|
+
match = LIBRARY_ROW.search(line)
|
|
162
|
+
if match:
|
|
163
|
+
registry["api_clients"].add(match.group(1))
|
|
164
|
+
elif current_section == "urls":
|
|
165
|
+
match = DOMAIN_ROW.search(line)
|
|
166
|
+
if match:
|
|
167
|
+
registry["urls"].add(match.group(1).lower())
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_registry(file_path: Path) -> dict[str, set[str]]:
|
|
171
|
+
"""Parse EXTERNAL_APIS.md to extract registered dependencies.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
file_path: Path to EXTERNAL_APIS.md file.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict with keys 'env_vars', 'api_clients', 'urls' mapping to sets of names.
|
|
178
|
+
"""
|
|
179
|
+
registry: dict[str, set[str]] = {
|
|
180
|
+
"env_vars": set(),
|
|
181
|
+
"api_clients": set(),
|
|
182
|
+
"urls": set(),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
content = file_path.read_text(encoding="utf-8")
|
|
187
|
+
except OSError as e:
|
|
188
|
+
logger.warning("registry_read_error", file=str(file_path), error=str(e))
|
|
189
|
+
return registry
|
|
190
|
+
|
|
191
|
+
lines = content.split("\n")
|
|
192
|
+
current_section = None
|
|
193
|
+
|
|
194
|
+
for line in lines:
|
|
195
|
+
# Detect section changes
|
|
196
|
+
new_section = _detect_section(line)
|
|
197
|
+
if new_section:
|
|
198
|
+
current_section = new_section
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if current_section:
|
|
202
|
+
_extract_registry_entry(line, current_section, registry)
|
|
203
|
+
|
|
204
|
+
logger.info(
|
|
205
|
+
"registry_parsed",
|
|
206
|
+
env_vars=len(registry["env_vars"]),
|
|
207
|
+
api_clients=len(registry["api_clients"]),
|
|
208
|
+
urls=len(registry["urls"]),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return registry
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _match_dependency_group(
|
|
215
|
+
registry_set: set[str],
|
|
216
|
+
detected_map: dict[str, str],
|
|
217
|
+
dep_type: str,
|
|
218
|
+
report: LinkReport,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Match a group of dependencies against registry.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
registry_set: Set of registered names.
|
|
224
|
+
detected_map: Map of detected names to locations.
|
|
225
|
+
dep_type: Type of dependency (e.g., 'env_var').
|
|
226
|
+
report: Report to update.
|
|
227
|
+
"""
|
|
228
|
+
# Match registered items
|
|
229
|
+
for name in registry_set:
|
|
230
|
+
if name in detected_map:
|
|
231
|
+
report.matched.append(name)
|
|
232
|
+
else:
|
|
233
|
+
report.stale.append(
|
|
234
|
+
DependencyGap(
|
|
235
|
+
gap_type="stale",
|
|
236
|
+
dep_type=dep_type,
|
|
237
|
+
name=name,
|
|
238
|
+
notes="In registry but not detected in code",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Find unregistered items
|
|
243
|
+
for name, location in detected_map.items():
|
|
244
|
+
if name not in registry_set:
|
|
245
|
+
report.unregistered.append(
|
|
246
|
+
DependencyGap(
|
|
247
|
+
gap_type="unregistered",
|
|
248
|
+
dep_type=dep_type,
|
|
249
|
+
name=name,
|
|
250
|
+
location=location,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def link_dependencies(
|
|
256
|
+
registry_file: str | Path,
|
|
257
|
+
code_directory: str | Path,
|
|
258
|
+
) -> LinkReport:
|
|
259
|
+
"""Compare registry against detected dependencies.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
registry_file: Path to EXTERNAL_APIS.md.
|
|
263
|
+
code_directory: Directory to scan for dependencies.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
LinkReport with matched, unregistered, and stale entries.
|
|
267
|
+
"""
|
|
268
|
+
report = LinkReport()
|
|
269
|
+
|
|
270
|
+
# Parse registry
|
|
271
|
+
registry_path = Path(registry_file)
|
|
272
|
+
if not registry_path.exists():
|
|
273
|
+
logger.warning("registry_not_found", file=str(registry_file))
|
|
274
|
+
# Scan anyway to report all as unregistered
|
|
275
|
+
registry: dict[str, set[str]] = {
|
|
276
|
+
"env_vars": set(),
|
|
277
|
+
"api_clients": set(),
|
|
278
|
+
"urls": set(),
|
|
279
|
+
}
|
|
280
|
+
else:
|
|
281
|
+
registry = parse_registry(registry_path)
|
|
282
|
+
|
|
283
|
+
report.registry_count = sum(len(v) for v in registry.values())
|
|
284
|
+
|
|
285
|
+
# Scan codebase
|
|
286
|
+
scan_result = scan_directory(code_directory)
|
|
287
|
+
report.detected_count = len(scan_result.dependencies)
|
|
288
|
+
|
|
289
|
+
# Build sets of detected items
|
|
290
|
+
detected_env_vars: dict[str, str] = {}
|
|
291
|
+
detected_clients: dict[str, str] = {}
|
|
292
|
+
detected_urls: dict[str, str] = {}
|
|
293
|
+
|
|
294
|
+
for dep in scan_result.dependencies:
|
|
295
|
+
location = f"{Path(dep.source_file).name}:{dep.line_number or '?'}"
|
|
296
|
+
if dep.dep_type == "env_var":
|
|
297
|
+
detected_env_vars[dep.name] = location
|
|
298
|
+
elif dep.dep_type == "api_client":
|
|
299
|
+
detected_clients[dep.name] = location
|
|
300
|
+
elif dep.dep_type == "url":
|
|
301
|
+
# Normalize URL to domain
|
|
302
|
+
detected_urls[dep.name.lower()] = location
|
|
303
|
+
|
|
304
|
+
# Match groups
|
|
305
|
+
_match_dependency_group(registry["env_vars"], detected_env_vars, "env_var", report)
|
|
306
|
+
_match_dependency_group(
|
|
307
|
+
registry["api_clients"], detected_clients, "api_client", report
|
|
308
|
+
)
|
|
309
|
+
_match_dependency_group(registry["urls"], detected_urls, "url", report)
|
|
310
|
+
|
|
311
|
+
logger.info(
|
|
312
|
+
"link_complete",
|
|
313
|
+
matched=len(report.matched),
|
|
314
|
+
unregistered=len(report.unregistered),
|
|
315
|
+
stale=len(report.stale),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return report
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def validate_external_deps(registry_file: str, code_directory: str) -> str:
|
|
322
|
+
"""Compare EXTERNAL_APIS.md against detected dependencies.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
registry_file: Path to EXTERNAL_APIS.md.
|
|
326
|
+
code_directory: Directory to scan.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Markdown report with gaps.
|
|
330
|
+
"""
|
|
331
|
+
report = link_dependencies(registry_file, code_directory)
|
|
332
|
+
markdown = report.to_markdown()
|
|
333
|
+
|
|
334
|
+
# Write artifact
|
|
335
|
+
output_path = Path(code_directory) / "EXTERNAL_DEPS_REPORT.md"
|
|
336
|
+
try:
|
|
337
|
+
output_path.write_text(markdown, encoding="utf-8")
|
|
338
|
+
return f"Generated: `{output_path}`\n\n{markdown}"
|
|
339
|
+
except OSError as e:
|
|
340
|
+
return f"Failed to write artifact: {e}\n\n{markdown}"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_unregistered_deps(code_directory: str) -> str:
|
|
344
|
+
"""Find external dependencies not in any registry.
|
|
345
|
+
|
|
346
|
+
Scans code without requiring EXTERNAL_APIS.md.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
code_directory: Directory to scan.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Summary of detected dependencies.
|
|
353
|
+
"""
|
|
354
|
+
scan_result = scan_directory(code_directory)
|
|
355
|
+
|
|
356
|
+
if not scan_result.dependencies:
|
|
357
|
+
return "No external dependencies detected."
|
|
358
|
+
|
|
359
|
+
lines = [
|
|
360
|
+
"# Detected External Dependencies",
|
|
361
|
+
"",
|
|
362
|
+
f"*Scanned {scan_result.files_scanned} files*",
|
|
363
|
+
"",
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
if scan_result.env_vars:
|
|
367
|
+
lines.append("## Environment Variables")
|
|
368
|
+
lines.append("")
|
|
369
|
+
for dep in scan_result.env_vars:
|
|
370
|
+
lines.append(
|
|
371
|
+
f"- `{dep.name}` ({Path(dep.source_file).name}:{dep.line_number})"
|
|
372
|
+
)
|
|
373
|
+
lines.append("")
|
|
374
|
+
|
|
375
|
+
if scan_result.api_clients:
|
|
376
|
+
lines.append("## API Clients")
|
|
377
|
+
lines.append("")
|
|
378
|
+
for dep in scan_result.api_clients:
|
|
379
|
+
lines.append(f"- `{dep.name}` — {dep.description}")
|
|
380
|
+
lines.append("")
|
|
381
|
+
|
|
382
|
+
if scan_result.urls:
|
|
383
|
+
lines.append("## External URLs")
|
|
384
|
+
lines.append("")
|
|
385
|
+
for dep in scan_result.urls:
|
|
386
|
+
lines.append(f"- `{dep.name}` ({Path(dep.source_file).name})")
|
|
387
|
+
lines.append("")
|
|
388
|
+
|
|
389
|
+
lines.append("---")
|
|
390
|
+
lines.append("")
|
|
391
|
+
lines.append("To register these, add them to `docs/templates/EXTERNAL_APIS.md`.")
|
|
392
|
+
|
|
393
|
+
return "\n".join(lines)
|