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,119 @@
|
|
|
1
|
+
"""Validation gate evaluation functions for quantifying AC quality metrics."""
|
|
2
|
+
|
|
3
|
+
from typing import TypedDict
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
logger = structlog.get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _count_matches_in_list(items: list[str], keywords: list[str]) -> int:
|
|
11
|
+
"""Count items that contain any of the keywords.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
items: List of strings to search
|
|
15
|
+
keywords: Keywords to search for
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Count of items containing at least one keyword
|
|
19
|
+
"""
|
|
20
|
+
return sum(
|
|
21
|
+
1 for item in items if any(keyword in item.lower() for keyword in keywords)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestStrategyResult(TypedDict):
|
|
26
|
+
"""Result of test strategy evaluation."""
|
|
27
|
+
|
|
28
|
+
has_test_strategy: bool
|
|
29
|
+
gherkin_ratio: float
|
|
30
|
+
gherkin_count: int
|
|
31
|
+
total_ac_count: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AmbiguityResult(TypedDict):
|
|
35
|
+
"""Result of ambiguity evaluation."""
|
|
36
|
+
|
|
37
|
+
is_ambiguous: bool
|
|
38
|
+
ambiguous_count: int
|
|
39
|
+
total_ac_count: int
|
|
40
|
+
ambiguous_terms_found: list[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def evaluate_test_strategy(acceptance_criteria: list[str]) -> TestStrategyResult:
|
|
44
|
+
"""Evaluate if acceptance criteria have a test strategy.
|
|
45
|
+
|
|
46
|
+
Uses Gherkin format detection (Given/When/Then keywords) to determine
|
|
47
|
+
if AC have a testable strategy. Threshold: gherkin_ratio >= 0.4.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
acceptance_criteria: List of acceptance criteria strings
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
TestStrategyResult with has_test_strategy boolean and metrics
|
|
54
|
+
"""
|
|
55
|
+
if not acceptance_criteria:
|
|
56
|
+
return TestStrategyResult(
|
|
57
|
+
has_test_strategy=False,
|
|
58
|
+
gherkin_ratio=0.0,
|
|
59
|
+
gherkin_count=0,
|
|
60
|
+
total_ac_count=0,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
gherkin_keywords = ["given", "when", "then", "and", "but"]
|
|
64
|
+
gherkin_count = _count_matches_in_list(acceptance_criteria, gherkin_keywords)
|
|
65
|
+
total_ac_count = len(acceptance_criteria)
|
|
66
|
+
gherkin_ratio = gherkin_count / total_ac_count
|
|
67
|
+
|
|
68
|
+
# Threshold: gherkin_ratio >= 0.4 = has_test_strategy=True
|
|
69
|
+
has_test_strategy = gherkin_ratio >= 0.4 # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
70
|
+
|
|
71
|
+
return TestStrategyResult(
|
|
72
|
+
has_test_strategy=has_test_strategy,
|
|
73
|
+
gherkin_ratio=gherkin_ratio,
|
|
74
|
+
gherkin_count=gherkin_count,
|
|
75
|
+
total_ac_count=total_ac_count,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def evaluate_ambiguous(acceptance_criteria: list[str]) -> AmbiguityResult:
|
|
80
|
+
"""Evaluate if acceptance criteria contain ambiguous terms.
|
|
81
|
+
|
|
82
|
+
Detects ambiguous terms that make AC untestable or unclear.
|
|
83
|
+
Uses same logic as invest_scorer._score_estimable().
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
acceptance_criteria: List of acceptance criteria strings
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
AmbiguityResult with is_ambiguous boolean and detected terms
|
|
90
|
+
"""
|
|
91
|
+
if not acceptance_criteria:
|
|
92
|
+
return AmbiguityResult(
|
|
93
|
+
is_ambiguous=False,
|
|
94
|
+
ambiguous_count=0,
|
|
95
|
+
total_ac_count=0,
|
|
96
|
+
ambiguous_terms_found=[],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
ambiguous_terms = ["fast", "quickly", "user-friendly", "easy", "intuitive"]
|
|
100
|
+
ambiguous_found: list[str] = []
|
|
101
|
+
|
|
102
|
+
for ac in acceptance_criteria:
|
|
103
|
+
ac_lower = ac.lower()
|
|
104
|
+
for term in ambiguous_terms:
|
|
105
|
+
if term in ac_lower and term not in ambiguous_found:
|
|
106
|
+
ambiguous_found.append(term)
|
|
107
|
+
|
|
108
|
+
ambiguous_count = len(ambiguous_found)
|
|
109
|
+
total_ac_count = len(acceptance_criteria)
|
|
110
|
+
|
|
111
|
+
# If any ambiguous terms found, AC is ambiguous
|
|
112
|
+
is_ambiguous = ambiguous_count > 0
|
|
113
|
+
|
|
114
|
+
return AmbiguityResult(
|
|
115
|
+
is_ambiguous=is_ambiguous,
|
|
116
|
+
ambiguous_count=ambiguous_count,
|
|
117
|
+
total_ac_count=total_ac_count,
|
|
118
|
+
ambiguous_terms_found=ambiguous_found,
|
|
119
|
+
)
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""External Dependency Detector for REF Agents.
|
|
2
|
+
|
|
3
|
+
Scans codebases to auto-detect external dependencies:
|
|
4
|
+
- Environment variable references (API URLs, endpoints)
|
|
5
|
+
- Known third-party API client libraries
|
|
6
|
+
- Hardcoded URL literals
|
|
7
|
+
- Config file endpoint definitions
|
|
8
|
+
- NuGet packages from .csproj and packages.config (C#)
|
|
9
|
+
|
|
10
|
+
Never touches .env files (security).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import re
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import structlog
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
# Known external API client libraries
|
|
24
|
+
KNOWN_API_CLIENTS: dict[str, str] = {
|
|
25
|
+
"stripe": "Stripe Payment API",
|
|
26
|
+
"boto3": "AWS Services",
|
|
27
|
+
"botocore": "AWS Services",
|
|
28
|
+
"twilio": "Twilio Communication API",
|
|
29
|
+
"sendgrid": "SendGrid Email API",
|
|
30
|
+
"slack_sdk": "Slack API",
|
|
31
|
+
"google.cloud": "Google Cloud Services",
|
|
32
|
+
"azure": "Azure Services",
|
|
33
|
+
"requests_oauthlib": "OAuth Provider",
|
|
34
|
+
"httpx": "HTTP Client (external calls)",
|
|
35
|
+
"aiohttp": "Async HTTP Client (external calls)",
|
|
36
|
+
"requests": "HTTP Client (external calls)",
|
|
37
|
+
"urllib3": "HTTP Client (external calls)",
|
|
38
|
+
"redis": "Redis Database",
|
|
39
|
+
"pymongo": "MongoDB",
|
|
40
|
+
"psycopg2": "PostgreSQL",
|
|
41
|
+
"mysql": "MySQL",
|
|
42
|
+
"elasticsearch": "Elasticsearch",
|
|
43
|
+
"kafka": "Apache Kafka",
|
|
44
|
+
"pika": "RabbitMQ",
|
|
45
|
+
"celery": "Celery Task Queue",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Patterns for env var extraction
|
|
49
|
+
ENV_VAR_PATTERNS: list[str] = [
|
|
50
|
+
r'os\.getenv\(["\']([A-Z_]+(?:URL|API|ENDPOINT|HOST|SERVICE)[A-Z_]*)["\']',
|
|
51
|
+
r'os\.environ\[["\']([A-Z_]+(?:URL|API|ENDPOINT|HOST|SERVICE)[A-Z_]*)["\']',
|
|
52
|
+
r'os\.environ\.get\(["\']([A-Z_]+(?:URL|API|ENDPOINT|HOST|SERVICE)[A-Z_]*)["\']',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Known external NuGet package prefixes → description
|
|
56
|
+
KNOWN_NUGET_CLIENTS: dict[str, str] = {
|
|
57
|
+
"Stripe.net": "Stripe Payment API",
|
|
58
|
+
"AWSSDK": "AWS Services",
|
|
59
|
+
"Twilio": "Twilio Communication API",
|
|
60
|
+
"SendGrid": "SendGrid Email API",
|
|
61
|
+
"SlackAPI": "Slack API",
|
|
62
|
+
"Google.Cloud": "Google Cloud Services",
|
|
63
|
+
"Azure": "Azure Services",
|
|
64
|
+
"Microsoft.Azure": "Azure Services",
|
|
65
|
+
"StackExchange.Redis": "Redis Database",
|
|
66
|
+
"MongoDB.Driver": "MongoDB",
|
|
67
|
+
"Npgsql": "PostgreSQL",
|
|
68
|
+
"MySql.Data": "MySQL",
|
|
69
|
+
"MySqlConnector": "MySQL",
|
|
70
|
+
"Elasticsearch.Net": "Elasticsearch",
|
|
71
|
+
"NEST": "Elasticsearch",
|
|
72
|
+
"Confluent.Kafka": "Apache Kafka",
|
|
73
|
+
"RabbitMQ.Client": "RabbitMQ",
|
|
74
|
+
"MassTransit": "Message Bus",
|
|
75
|
+
"Refit": "HTTP Client (typed REST)",
|
|
76
|
+
"RestSharp": "HTTP Client (external calls)",
|
|
77
|
+
"Flurl": "HTTP Client (external calls)",
|
|
78
|
+
"Polly": "Resilience / Retry Policy",
|
|
79
|
+
"Serilog": "Structured Logging",
|
|
80
|
+
"NLog": "Logging",
|
|
81
|
+
"Hangfire": "Background Jobs",
|
|
82
|
+
"Quartz": "Job Scheduler",
|
|
83
|
+
"PayPal": "PayPal Payment API",
|
|
84
|
+
"Braintree": "Braintree Payment API",
|
|
85
|
+
"Okta": "Okta Identity API",
|
|
86
|
+
"Auth0": "Auth0 Identity API",
|
|
87
|
+
"Datadog": "Datadog APM",
|
|
88
|
+
"OpenTelemetry": "OpenTelemetry",
|
|
89
|
+
"NewRelic": "New Relic APM",
|
|
90
|
+
"Sentry": "Sentry Error Tracking",
|
|
91
|
+
"LaunchDarkly": "Feature Flags",
|
|
92
|
+
"Contentful": "Contentful CMS API",
|
|
93
|
+
"Algolia": "Algolia Search API",
|
|
94
|
+
"Segment": "Segment Analytics",
|
|
95
|
+
"Mixpanel": "Mixpanel Analytics",
|
|
96
|
+
"SendinBlue": "SendinBlue Email API",
|
|
97
|
+
"PusherServer": "Pusher WebSocket API",
|
|
98
|
+
"FirebaseAdmin": "Firebase API",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# URL pattern
|
|
102
|
+
URL_PATTERN = re.compile(
|
|
103
|
+
r'["\']'
|
|
104
|
+
r'(https?://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[^\s"\']*)'
|
|
105
|
+
r'["\']'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class ExternalDependency:
|
|
111
|
+
"""Represents a detected external dependency."""
|
|
112
|
+
|
|
113
|
+
name: str
|
|
114
|
+
dep_type: str # 'env_var', 'api_client', 'url', 'config'
|
|
115
|
+
source_file: str
|
|
116
|
+
line_number: int | None = None
|
|
117
|
+
description: str = ""
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict[str, str | int | None]:
|
|
120
|
+
"""Convert to dictionary."""
|
|
121
|
+
return {
|
|
122
|
+
"name": self.name,
|
|
123
|
+
"type": self.dep_type,
|
|
124
|
+
"file": self.source_file,
|
|
125
|
+
"line": self.line_number,
|
|
126
|
+
"description": self.description,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class ScanResult:
|
|
132
|
+
"""Result of external dependency scan."""
|
|
133
|
+
|
|
134
|
+
dependencies: list[ExternalDependency] = field(default_factory=list)
|
|
135
|
+
files_scanned: int = 0
|
|
136
|
+
errors: list[str] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def env_vars(self) -> list[ExternalDependency]:
|
|
140
|
+
"""Get environment variable dependencies."""
|
|
141
|
+
return [d for d in self.dependencies if d.dep_type == "env_var"]
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def api_clients(self) -> list[ExternalDependency]:
|
|
145
|
+
"""Get API client dependencies."""
|
|
146
|
+
return [d for d in self.dependencies if d.dep_type == "api_client"]
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def urls(self) -> list[ExternalDependency]:
|
|
150
|
+
"""Get URL dependencies."""
|
|
151
|
+
return [d for d in self.dependencies if d.dep_type == "url"]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def nuget_packages(self) -> list[ExternalDependency]:
|
|
155
|
+
"""Get NuGet package dependencies."""
|
|
156
|
+
return [d for d in self.dependencies if d.dep_type == "nuget_package"]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _scan_file_for_env_vars(file_path: Path, content: str) -> list[ExternalDependency]:
|
|
160
|
+
"""Scan file content for environment variable references.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
file_path: Path to the file being scanned.
|
|
164
|
+
content: File content.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of detected env var dependencies.
|
|
168
|
+
"""
|
|
169
|
+
deps: list[ExternalDependency] = []
|
|
170
|
+
seen: set[str] = set()
|
|
171
|
+
|
|
172
|
+
for pattern in ENV_VAR_PATTERNS:
|
|
173
|
+
for match in re.finditer(pattern, content):
|
|
174
|
+
var_name = match.group(1)
|
|
175
|
+
if var_name not in seen:
|
|
176
|
+
seen.add(var_name)
|
|
177
|
+
# Find line number
|
|
178
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
179
|
+
deps.append(
|
|
180
|
+
ExternalDependency(
|
|
181
|
+
name=var_name,
|
|
182
|
+
dep_type="env_var",
|
|
183
|
+
source_file=str(file_path),
|
|
184
|
+
line_number=line_num,
|
|
185
|
+
description="Environment variable for external endpoint",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return deps
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _scan_file_for_api_clients(
|
|
193
|
+
file_path: Path, content: str
|
|
194
|
+
) -> list[ExternalDependency]:
|
|
195
|
+
"""Scan file for known API client library imports.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
file_path: Path to the file being scanned.
|
|
199
|
+
content: File content.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of detected API client dependencies.
|
|
203
|
+
"""
|
|
204
|
+
deps: list[ExternalDependency] = []
|
|
205
|
+
seen: set[str] = set()
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
tree = ast.parse(content)
|
|
209
|
+
except SyntaxError:
|
|
210
|
+
return deps
|
|
211
|
+
|
|
212
|
+
for node in ast.walk(tree):
|
|
213
|
+
module_name: str | None = None
|
|
214
|
+
|
|
215
|
+
if isinstance(node, ast.Import):
|
|
216
|
+
for alias in node.names:
|
|
217
|
+
module_name = alias.name.split(".")[0]
|
|
218
|
+
elif isinstance(node, ast.ImportFrom):
|
|
219
|
+
if node.module:
|
|
220
|
+
module_name = node.module.split(".")[0]
|
|
221
|
+
|
|
222
|
+
if module_name and module_name in KNOWN_API_CLIENTS:
|
|
223
|
+
if module_name not in seen:
|
|
224
|
+
seen.add(module_name)
|
|
225
|
+
deps.append(
|
|
226
|
+
ExternalDependency(
|
|
227
|
+
name=module_name,
|
|
228
|
+
dep_type="api_client",
|
|
229
|
+
source_file=str(file_path),
|
|
230
|
+
line_number=getattr(node, "lineno", None),
|
|
231
|
+
description=KNOWN_API_CLIENTS[module_name],
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return deps
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _scan_file_for_urls(file_path: Path, content: str) -> list[ExternalDependency]:
|
|
239
|
+
"""Scan file for hardcoded external URLs.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
file_path: Path to the file being scanned.
|
|
243
|
+
content: File content.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of detected URL dependencies.
|
|
247
|
+
"""
|
|
248
|
+
deps: list[ExternalDependency] = []
|
|
249
|
+
seen: set[str] = set()
|
|
250
|
+
|
|
251
|
+
for match in URL_PATTERN.finditer(content):
|
|
252
|
+
url = match.group(1)
|
|
253
|
+
# Extract domain
|
|
254
|
+
domain_match = re.search(r"https?://([^/]+)", url)
|
|
255
|
+
domain = domain_match.group(1) if domain_match else url
|
|
256
|
+
|
|
257
|
+
if domain not in seen:
|
|
258
|
+
seen.add(domain)
|
|
259
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
260
|
+
deps.append(
|
|
261
|
+
ExternalDependency(
|
|
262
|
+
name=domain,
|
|
263
|
+
dep_type="url",
|
|
264
|
+
source_file=str(file_path),
|
|
265
|
+
line_number=line_num,
|
|
266
|
+
description=f"External URL: {url[:50]}...",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return deps
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _describe_nuget_package(package_id: str) -> str:
|
|
274
|
+
"""Return a human-readable description for a NuGet package ID.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
package_id: NuGet package identifier (e.g. 'StackExchange.Redis').
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Description from KNOWN_NUGET_CLIENTS or generic fallback.
|
|
281
|
+
"""
|
|
282
|
+
for prefix, description in KNOWN_NUGET_CLIENTS.items():
|
|
283
|
+
if package_id.startswith(prefix):
|
|
284
|
+
return description
|
|
285
|
+
return f"NuGet package: {package_id}"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _scan_csproj_for_packages(csproj_path: Path) -> list[ExternalDependency]:
|
|
289
|
+
"""Extract PackageReference entries from a .csproj file.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
csproj_path: Path to the .csproj file.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
List of ExternalDependency for each PackageReference found.
|
|
296
|
+
"""
|
|
297
|
+
deps: list[ExternalDependency] = []
|
|
298
|
+
try:
|
|
299
|
+
tree = ET.parse(csproj_path) # noqa: S314 — local file, not from network
|
|
300
|
+
except ET.ParseError as e:
|
|
301
|
+
logger.warning("csproj_parse_error", file=str(csproj_path), error=str(e))
|
|
302
|
+
return deps
|
|
303
|
+
except OSError as e:
|
|
304
|
+
logger.warning("csproj_read_error", file=str(csproj_path), error=str(e))
|
|
305
|
+
return deps
|
|
306
|
+
|
|
307
|
+
root = tree.getroot()
|
|
308
|
+
seen: set[str] = set()
|
|
309
|
+
|
|
310
|
+
# ElementTree strips namespace prefixes; .csproj elements are un-namespaced
|
|
311
|
+
for ref in root.iter("PackageReference"):
|
|
312
|
+
package_id = ref.get("Include") or ref.get("Update") or ""
|
|
313
|
+
if not package_id or package_id in seen:
|
|
314
|
+
continue
|
|
315
|
+
seen.add(package_id)
|
|
316
|
+
deps.append(
|
|
317
|
+
ExternalDependency(
|
|
318
|
+
name=package_id,
|
|
319
|
+
dep_type="nuget_package",
|
|
320
|
+
source_file=str(csproj_path),
|
|
321
|
+
line_number=None,
|
|
322
|
+
description=_describe_nuget_package(package_id),
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
logger.debug(
|
|
327
|
+
"csproj_scanned",
|
|
328
|
+
file=str(csproj_path),
|
|
329
|
+
packages_found=len(deps),
|
|
330
|
+
)
|
|
331
|
+
return deps
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _scan_packages_config(packages_config_path: Path) -> list[ExternalDependency]:
|
|
335
|
+
"""Extract package entries from a legacy packages.config file.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
packages_config_path: Path to the packages.config file.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of ExternalDependency for each package found.
|
|
342
|
+
"""
|
|
343
|
+
deps: list[ExternalDependency] = []
|
|
344
|
+
try:
|
|
345
|
+
tree = ET.parse(packages_config_path) # noqa: S314 — local file, not from network
|
|
346
|
+
except ET.ParseError as e:
|
|
347
|
+
logger.warning(
|
|
348
|
+
"packages_config_parse_error",
|
|
349
|
+
file=str(packages_config_path),
|
|
350
|
+
error=str(e),
|
|
351
|
+
)
|
|
352
|
+
return deps
|
|
353
|
+
except OSError as e:
|
|
354
|
+
logger.warning(
|
|
355
|
+
"packages_config_read_error",
|
|
356
|
+
file=str(packages_config_path),
|
|
357
|
+
error=str(e),
|
|
358
|
+
)
|
|
359
|
+
return deps
|
|
360
|
+
|
|
361
|
+
root = tree.getroot()
|
|
362
|
+
seen: set[str] = set()
|
|
363
|
+
|
|
364
|
+
for pkg in root.iter("package"):
|
|
365
|
+
package_id = pkg.get("id") or ""
|
|
366
|
+
if not package_id or package_id in seen:
|
|
367
|
+
continue
|
|
368
|
+
seen.add(package_id)
|
|
369
|
+
deps.append(
|
|
370
|
+
ExternalDependency(
|
|
371
|
+
name=package_id,
|
|
372
|
+
dep_type="nuget_package",
|
|
373
|
+
source_file=str(packages_config_path),
|
|
374
|
+
line_number=None,
|
|
375
|
+
description=_describe_nuget_package(package_id),
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
logger.debug(
|
|
380
|
+
"packages_config_scanned",
|
|
381
|
+
file=str(packages_config_path),
|
|
382
|
+
packages_found=len(deps),
|
|
383
|
+
)
|
|
384
|
+
return deps
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def scan_directory(directory: str | Path) -> ScanResult:
|
|
388
|
+
"""Scan a directory for external dependencies.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
directory: Path to directory to scan.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
ScanResult with all detected dependencies.
|
|
395
|
+
"""
|
|
396
|
+
result = ScanResult()
|
|
397
|
+
dir_path = Path(directory)
|
|
398
|
+
|
|
399
|
+
if not dir_path.exists():
|
|
400
|
+
result.errors.append(f"Directory not found: {directory}")
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
# Find all Python files, excluding common non-code directories
|
|
404
|
+
exclude_dirs = {
|
|
405
|
+
"__pycache__",
|
|
406
|
+
".git",
|
|
407
|
+
".venv",
|
|
408
|
+
"venv",
|
|
409
|
+
"node_modules",
|
|
410
|
+
".tox",
|
|
411
|
+
"dist",
|
|
412
|
+
"build",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for py_file in dir_path.rglob("*.py"):
|
|
416
|
+
# Skip excluded directories
|
|
417
|
+
if any(excluded in py_file.parts for excluded in exclude_dirs):
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
content = py_file.read_text(encoding="utf-8")
|
|
422
|
+
result.files_scanned += 1
|
|
423
|
+
|
|
424
|
+
# Scan for all dependency types
|
|
425
|
+
result.dependencies.extend(_scan_file_for_env_vars(py_file, content))
|
|
426
|
+
result.dependencies.extend(_scan_file_for_api_clients(py_file, content))
|
|
427
|
+
result.dependencies.extend(_scan_file_for_urls(py_file, content))
|
|
428
|
+
|
|
429
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
430
|
+
result.errors.append(f"Error reading {py_file}: {e}")
|
|
431
|
+
logger.warning("file_read_error", file=str(py_file), error=str(e))
|
|
432
|
+
|
|
433
|
+
# C# NuGet packages: modern SDK-style (.csproj PackageReference)
|
|
434
|
+
csproj_exclude = {"bin", "obj", ".git", "node_modules"}
|
|
435
|
+
for csproj_file in dir_path.rglob("*.csproj"):
|
|
436
|
+
if any(excluded in csproj_file.parts for excluded in csproj_exclude):
|
|
437
|
+
continue
|
|
438
|
+
result.dependencies.extend(_scan_csproj_for_packages(csproj_file))
|
|
439
|
+
result.files_scanned += 1
|
|
440
|
+
|
|
441
|
+
# C# NuGet packages: legacy packages.config (pre-SDK projects)
|
|
442
|
+
for pkg_config in dir_path.rglob("packages.config"):
|
|
443
|
+
if any(excluded in pkg_config.parts for excluded in csproj_exclude):
|
|
444
|
+
continue
|
|
445
|
+
result.dependencies.extend(_scan_packages_config(pkg_config))
|
|
446
|
+
result.files_scanned += 1
|
|
447
|
+
|
|
448
|
+
logger.info(
|
|
449
|
+
"scan_complete",
|
|
450
|
+
files_scanned=result.files_scanned,
|
|
451
|
+
dependencies_found=len(result.dependencies),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return result
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def generate_codemap_section(result: ScanResult) -> str:
|
|
458
|
+
"""Generate CODE_MAP.md External Dependencies section.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
result: Scan result with dependencies.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Markdown formatted section.
|
|
465
|
+
"""
|
|
466
|
+
output = """## External Dependencies
|
|
467
|
+
*Auto-detected by external_detector.py*
|
|
468
|
+
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
if not result.dependencies:
|
|
472
|
+
output += "No external dependencies detected.\n"
|
|
473
|
+
return output
|
|
474
|
+
|
|
475
|
+
# Group by type
|
|
476
|
+
if result.env_vars:
|
|
477
|
+
output += "### Environment Variables (External Endpoints)\n\n"
|
|
478
|
+
output += "| Variable | File | Line | Notes |\n"
|
|
479
|
+
output += "|----------|------|------|-------|\n"
|
|
480
|
+
for dep in result.env_vars:
|
|
481
|
+
output += f"| `{dep.name}` | `{Path(dep.source_file).name}` | {dep.line_number or '-'} | {dep.description} |\n"
|
|
482
|
+
output += "\n"
|
|
483
|
+
|
|
484
|
+
if result.api_clients:
|
|
485
|
+
output += "### External API Clients\n\n"
|
|
486
|
+
output += "| Library | Service | File | Line |\n"
|
|
487
|
+
output += "|---------|---------|------|------|\n"
|
|
488
|
+
for dep in result.api_clients:
|
|
489
|
+
output += f"| `{dep.name}` | {dep.description} | `{Path(dep.source_file).name}` | {dep.line_number or '-'} |\n"
|
|
490
|
+
output += "\n"
|
|
491
|
+
|
|
492
|
+
if result.urls:
|
|
493
|
+
output += "### Hardcoded External URLs\n\n"
|
|
494
|
+
output += "| Domain | File | Line | Notes |\n"
|
|
495
|
+
output += "|--------|------|------|-------|\n"
|
|
496
|
+
for dep in result.urls:
|
|
497
|
+
output += f"| `{dep.name}` | `{Path(dep.source_file).name}` | {dep.line_number or '-'} | ⚠️ Consider using env var |\n"
|
|
498
|
+
output += "\n"
|
|
499
|
+
|
|
500
|
+
return output
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def scan_and_report(directory: str) -> str:
|
|
504
|
+
"""Scan directory and return formatted report.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
directory: Path to scan.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Status message with summary.
|
|
511
|
+
"""
|
|
512
|
+
result = scan_directory(directory)
|
|
513
|
+
|
|
514
|
+
if result.errors:
|
|
515
|
+
return f"Scan completed with errors: {result.errors}"
|
|
516
|
+
|
|
517
|
+
summary = (
|
|
518
|
+
f"Scanned {result.files_scanned} files. "
|
|
519
|
+
f"Found: {len(result.env_vars)} env vars, "
|
|
520
|
+
f"{len(result.api_clients)} API clients, "
|
|
521
|
+
f"{len(result.urls)} URLs."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return summary
|