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,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)