vcase-auto-context 0.1.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.
- vcase/__init__.py +60 -0
- vcase/api/__init__.py +1 -0
- vcase/api/github_client.py +24 -0
- vcase/api/npm_client.py +34 -0
- vcase/api/pypi_client.py +36 -0
- vcase/api/web_scraper.py +25 -0
- vcase/cli.py +198 -0
- vcase/main.py +21 -0
- vcase/providers/__init__.py +1 -0
- vcase/providers/base.py +132 -0
- vcase/providers/node_provider.py +167 -0
- vcase/providers/python_provider.py +218 -0
- vcase/routers/__init__.py +1 -0
- vcase/routers/health.py +17 -0
- vcase/routers/proxy.py +241 -0
- vcase/service/__init__.py +1 -0
- vcase/service/api_index_cache.py +280 -0
- vcase/service/api_validator.py +159 -0
- vcase/service/architecture_validator.py +136 -0
- vcase/service/ast_parser.py +234 -0
- vcase/service/contract_extractor.py +67 -0
- vcase/service/detector.py +133 -0
- vcase/service/interaction_graph.py +128 -0
- vcase/service/llm_validator.py +282 -0
- vcase/service/orchestrator.py +211 -0
- vcase/service/resolver.py +64 -0
- vcase/service/retriever.py +45 -0
- vcase/service/spec_builder.py +129 -0
- vcase/service/workspace_scanner.py +101 -0
- vcase/validator/__init__.py +1 -0
- vcase/validator/adapters/python_ast.py +166 -0
- vcase/validator/adapters/ts_ast.py +23 -0
- vcase/validator/core/ir.py +37 -0
- vcase/validator/core/rules_engine.py +122 -0
- vcase/validator/specs/api_contracts.json +12 -0
- vcase/validator/specs/architecture_rules.json +23 -0
- vcase_auto_context-0.1.0.dist-info/METADATA +82 -0
- vcase_auto_context-0.1.0.dist-info/RECORD +41 -0
- vcase_auto_context-0.1.0.dist-info/WHEEL +5 -0
- vcase_auto_context-0.1.0.dist-info/entry_points.txt +2 -0
- vcase_auto_context-0.1.0.dist-info/top_level.txt +1 -0
vcase/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from vcase.api.pypi_client import PypiClient
|
|
7
|
+
from vcase.api.npm_client import NpmClient
|
|
8
|
+
from vcase.service.api_index_cache import ApiIndexCache
|
|
9
|
+
from vcase.providers.python_provider import PythonProvider
|
|
10
|
+
from vcase.providers.node_provider import NodeProvider
|
|
11
|
+
from vcase.service.detector import TechnologyDetector
|
|
12
|
+
from vcase.service.resolver import DependencyResolver
|
|
13
|
+
from vcase.service.retriever import DocumentationRetriever
|
|
14
|
+
from vcase.service.spec_builder import SpecBuilder
|
|
15
|
+
from vcase.service.api_validator import ApiValidator
|
|
16
|
+
from vcase.service.architecture_validator import ArchitectureValidator
|
|
17
|
+
from vcase.service.llm_validator import LlmValidator
|
|
18
|
+
from vcase.service.orchestrator import Orchestrator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_orchestrator(
|
|
22
|
+
http_client: httpx.AsyncClient,
|
|
23
|
+
model_name: str | None = None,
|
|
24
|
+
api_key: str | None = None,
|
|
25
|
+
provider: str | None = None,
|
|
26
|
+
) -> Orchestrator:
|
|
27
|
+
"""Helper factory function to construct a fully configured Orchestrator pipeline."""
|
|
28
|
+
pypi_client = PypiClient(http_client)
|
|
29
|
+
npm_client = NpmClient(http_client)
|
|
30
|
+
cache = ApiIndexCache(http_client)
|
|
31
|
+
|
|
32
|
+
python_prov = PythonProvider(pypi_client, cache)
|
|
33
|
+
node_prov = NodeProvider(npm_client)
|
|
34
|
+
|
|
35
|
+
detector = TechnologyDetector([python_prov, node_prov])
|
|
36
|
+
resolver = DependencyResolver([python_prov, node_prov])
|
|
37
|
+
retriever = DocumentationRetriever([python_prov, node_prov])
|
|
38
|
+
|
|
39
|
+
spec_builder = SpecBuilder()
|
|
40
|
+
api_validator = ApiValidator()
|
|
41
|
+
arch_validator = ArchitectureValidator()
|
|
42
|
+
|
|
43
|
+
llm_validator = LlmValidator(
|
|
44
|
+
model_name=model_name,
|
|
45
|
+
api_key=api_key,
|
|
46
|
+
provider=provider
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return Orchestrator(
|
|
50
|
+
detector=detector,
|
|
51
|
+
resolver=resolver,
|
|
52
|
+
retriever=retriever,
|
|
53
|
+
spec_builder=spec_builder,
|
|
54
|
+
api_validator=api_validator,
|
|
55
|
+
architecture_validator=arch_validator,
|
|
56
|
+
llm_validator=llm_validator,
|
|
57
|
+
model_name=model_name,
|
|
58
|
+
api_key=api_key,
|
|
59
|
+
provider=provider
|
|
60
|
+
)
|
vcase/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
GITHUB_API_BASE_URL = "https://api.github.com"
|
|
10
|
+
REQUEST_TIMEOUT_SECONDS = 10
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GithubClient:
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: httpx.AsyncClient, api_token: str | None = None) -> None:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
async def get_release_notes(self, owner: str, repo: str, tag: str) -> str:
|
|
19
|
+
"""Return release notes markdown for a given tag."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def get_readme(self, owner: str, repo: str, ref: str) -> str:
|
|
23
|
+
"""Return the README content for a given ref (tag or branch)."""
|
|
24
|
+
...
|
vcase/api/npm_client.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org"
|
|
10
|
+
REQUEST_TIMEOUT_SECONDS = 10
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NpmClient:
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: httpx.AsyncClient) -> None:
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
async def get_package_info(self, package: str, version: str) -> dict:
|
|
19
|
+
"""Fetch package metadata from npm registry. Raises on non-200 response."""
|
|
20
|
+
url = f"{NPM_REGISTRY_BASE_URL}/{package}/{version}"
|
|
21
|
+
response = await self._client.get(url, timeout=REQUEST_TIMEOUT_SECONDS)
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
return response.json()
|
|
24
|
+
|
|
25
|
+
async def get_latest_version(self, package: str) -> str:
|
|
26
|
+
"""Return the latest published version string for a package."""
|
|
27
|
+
url = f"{NPM_REGISTRY_BASE_URL}/{package}"
|
|
28
|
+
response = await self._client.get(url, timeout=REQUEST_TIMEOUT_SECONDS)
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
data = response.json()
|
|
31
|
+
version = data.get("dist-tags", {}).get("latest", "")
|
|
32
|
+
if not version:
|
|
33
|
+
logger.warning("Could not determine latest version for npm package %s", package)
|
|
34
|
+
return version
|
vcase/api/pypi_client.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
PYPI_BASE_URL = "https://pypi.org/pypi"
|
|
10
|
+
REQUEST_TIMEOUT_SECONDS = 10
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PypiClient:
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: httpx.AsyncClient) -> None:
|
|
16
|
+
self._client=client
|
|
17
|
+
|
|
18
|
+
async def get_package_info(self, package: str, version: str) -> dict:
|
|
19
|
+
"""Fetch package metadata from PyPI. Raises on non-200 response."""
|
|
20
|
+
url=f"{PYPI_BASE_URL}/{package}/{version}/json"
|
|
21
|
+
response=await self._client.get(url,timeout=REQUEST_TIMEOUT_SECONDS)
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
return response.json()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_latest_version(self, package: str) -> str:
|
|
27
|
+
"""Return the latest published version string for a package."""
|
|
28
|
+
url=f"{PYPI_BASE_URL}/{package}/json"
|
|
29
|
+
response=await self._client.get(url,timeout=REQUEST_TIMEOUT_SECONDS)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
data=response.json()
|
|
32
|
+
version= data.get("info",{}).get("version","")
|
|
33
|
+
if not version:
|
|
34
|
+
logger.warning("Could not determine latest version for %s",package)
|
|
35
|
+
return version
|
|
36
|
+
|
vcase/api/web_scraper.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
REQUEST_TIMEOUT_SECONDS = 15
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WebScraper:
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: httpx.AsyncClient) -> None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
async def fetch_as_markdown(self, url: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Fetch a URL and return its content as clean markdown.
|
|
20
|
+
Strips navigation, ads, and irrelevant HTML. Raises on failure.
|
|
21
|
+
"""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def _html_to_markdown(self, html: str) -> str:
|
|
25
|
+
...
|
vcase/cli.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import httpx
|
|
9
|
+
import uvicorn
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
from vcase import create_orchestrator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
# Load environment variables
|
|
17
|
+
load_dotenv(override=True)
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Version-Constrained API Specification Engine (VCASE)"
|
|
20
|
+
)
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
# serve command
|
|
24
|
+
serve_parser = subparsers.add_parser("serve", help="Start the VCASE FastAPI server")
|
|
25
|
+
serve_parser.add_argument("--host", default="127.0.0.1", help="Host binding address")
|
|
26
|
+
serve_parser.add_argument("--port", type=int, default=8000, help="Port to listen on")
|
|
27
|
+
serve_parser.add_argument("--reload", action="store_true", help="Enable hot reloading")
|
|
28
|
+
|
|
29
|
+
# run command
|
|
30
|
+
run_parser = subparsers.add_parser("run", help="Generate and validate code directly from the CLI")
|
|
31
|
+
run_parser.add_argument("--prompt", required=True, help="User instructions / prompt")
|
|
32
|
+
run_parser.add_argument("--repo-path", required=True, help="Path to local repository to inspect stack/versions")
|
|
33
|
+
run_parser.add_argument("--target-file", help="Optional relative path to target file to generate/edit")
|
|
34
|
+
run_parser.add_argument("--api-key", help="API key for the selected LLM provider")
|
|
35
|
+
run_parser.add_argument("--model", help="Model name to use for generation and validation")
|
|
36
|
+
run_parser.add_argument("--provider", choices=["openai", "gemini", "openrouter", "groq"], help="LLM API provider")
|
|
37
|
+
|
|
38
|
+
# context command
|
|
39
|
+
context_parser = subparsers.add_parser("context", help="Retrieve versioned specification context for code generation")
|
|
40
|
+
context_parser.add_argument("--prompt", required=True, help="User prompt or instructions")
|
|
41
|
+
context_parser.add_argument("--repo-path", required=True, help="Path to local repository to inspect stack/versions")
|
|
42
|
+
|
|
43
|
+
# validate command
|
|
44
|
+
validate_parser = subparsers.add_parser("validate", help="Validate/lint code against dependency contracts and rules")
|
|
45
|
+
validate_parser.add_argument("--code-file", required=True, help="Path to Python file containing code to validate")
|
|
46
|
+
validate_parser.add_argument("--repo-path", required=True, help="Path to local repository")
|
|
47
|
+
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.command == "serve":
|
|
51
|
+
print(f"Starting VCASE API proxy server on http://{args.host}:{args.port}...")
|
|
52
|
+
# Since uvicorn.run expects target app, we pass the package module target path
|
|
53
|
+
uvicorn.run("vcase.main:app", host=args.host, port=args.port, reload=args.reload)
|
|
54
|
+
|
|
55
|
+
elif args.command == "run":
|
|
56
|
+
repo_path = Path(args.repo_path)
|
|
57
|
+
if not repo_path.exists():
|
|
58
|
+
print(f"Error: Repository path '{args.repo_path}' does not exist.", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
async def run_pipeline():
|
|
62
|
+
async with httpx.AsyncClient() as http_client:
|
|
63
|
+
orchestrator = create_orchestrator(
|
|
64
|
+
http_client,
|
|
65
|
+
model_name=args.model,
|
|
66
|
+
api_key=args.api_key,
|
|
67
|
+
provider=args.provider
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
# Execute orchestrator pipeline
|
|
71
|
+
target_file = Path(args.target_file) if args.target_file else None
|
|
72
|
+
code = await orchestrator.run(args.prompt, repo_path, target_file=target_file)
|
|
73
|
+
print("\n--- GENERATED & VALIDATED CODE ---")
|
|
74
|
+
print(code)
|
|
75
|
+
print("----------------------------------\n")
|
|
76
|
+
print("Status: Validation Passed.")
|
|
77
|
+
|
|
78
|
+
if target_file:
|
|
79
|
+
target_file_path = repo_path / target_file if not target_file.is_absolute() else target_file
|
|
80
|
+
# Ensure directories exist
|
|
81
|
+
target_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
target_file_path.write_text(code, encoding="utf-8")
|
|
83
|
+
print(f"Saved generated code directly to: {target_file_path}")
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
print(f"\nValidation Failed:\n{e}", file=sys.stderr)
|
|
86
|
+
sys.exit(2)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"\nOrchestrator Execution Failed:\n{e}", file=sys.stderr)
|
|
89
|
+
sys.exit(3)
|
|
90
|
+
|
|
91
|
+
asyncio.run(run_pipeline())
|
|
92
|
+
|
|
93
|
+
elif args.command == "context":
|
|
94
|
+
repo_path = Path(args.repo_path)
|
|
95
|
+
if not repo_path.exists():
|
|
96
|
+
print(f"Error: Repository path '{args.repo_path}' does not exist.", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
async def get_context():
|
|
100
|
+
async with httpx.AsyncClient() as http_client:
|
|
101
|
+
orchestrator = create_orchestrator(http_client)
|
|
102
|
+
try:
|
|
103
|
+
techs = orchestrator._detector.detect_technologies_in_prompt(args.prompt)
|
|
104
|
+
resolved_deps = await orchestrator._resolver.resolve(repo_path)
|
|
105
|
+
for dep in resolved_deps:
|
|
106
|
+
if dep.name not in techs:
|
|
107
|
+
techs.append(dep.name)
|
|
108
|
+
|
|
109
|
+
api_indexes = []
|
|
110
|
+
for dep in resolved_deps:
|
|
111
|
+
idx = await orchestrator._retriever.retrieve(dep)
|
|
112
|
+
if idx:
|
|
113
|
+
api_indexes.append(idx)
|
|
114
|
+
|
|
115
|
+
# Scan workspace
|
|
116
|
+
from vcase.service.workspace_scanner import WorkspaceScanner
|
|
117
|
+
scanner = WorkspaceScanner()
|
|
118
|
+
workspace_idx, _ = scanner.scan(repo_path)
|
|
119
|
+
if workspace_idx and workspace_idx.signatures:
|
|
120
|
+
api_indexes.append(workspace_idx)
|
|
121
|
+
|
|
122
|
+
from vcase.service.interaction_graph import get_active_patterns
|
|
123
|
+
patterns = get_active_patterns(techs)
|
|
124
|
+
|
|
125
|
+
context_prompt = orchestrator._spec_builder.build_system_prompt(
|
|
126
|
+
api_indexes, patterns, user_prompt=args.prompt
|
|
127
|
+
)
|
|
128
|
+
print(context_prompt)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Error gathering context: {e}", file=sys.stderr)
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
asyncio.run(get_context())
|
|
134
|
+
|
|
135
|
+
elif args.command == "validate":
|
|
136
|
+
repo_path = Path(args.repo_path)
|
|
137
|
+
code_file = Path(args.code_file)
|
|
138
|
+
if not repo_path.exists():
|
|
139
|
+
print(f"Error: Repository path '{args.repo_path}' does not exist.", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
if not code_file.exists():
|
|
142
|
+
print(f"Error: Code file '{args.code_file}' does not exist.", file=sys.stderr)
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
async def run_validation():
|
|
146
|
+
async with httpx.AsyncClient() as http_client:
|
|
147
|
+
orchestrator = create_orchestrator(http_client)
|
|
148
|
+
try:
|
|
149
|
+
code = code_file.read_text(encoding="utf-8")
|
|
150
|
+
resolved_deps = await orchestrator._resolver.resolve(repo_path)
|
|
151
|
+
techs = [dep.name for dep in resolved_deps]
|
|
152
|
+
|
|
153
|
+
api_indexes = []
|
|
154
|
+
for dep in resolved_deps:
|
|
155
|
+
idx = await orchestrator._retriever.retrieve(dep)
|
|
156
|
+
if idx:
|
|
157
|
+
api_indexes.append(idx)
|
|
158
|
+
|
|
159
|
+
# Scan workspace
|
|
160
|
+
from vcase.service.workspace_scanner import WorkspaceScanner
|
|
161
|
+
scanner = WorkspaceScanner()
|
|
162
|
+
workspace_idx, local_modules = scanner.scan(repo_path)
|
|
163
|
+
if workspace_idx and workspace_idx.signatures:
|
|
164
|
+
api_indexes.append(workspace_idx)
|
|
165
|
+
|
|
166
|
+
from vcase.service.interaction_graph import get_constraints_for, get_active_patterns
|
|
167
|
+
patterns = get_active_patterns(techs)
|
|
168
|
+
constraints = []
|
|
169
|
+
for tech in techs:
|
|
170
|
+
constraints.extend(get_constraints_for(tech))
|
|
171
|
+
|
|
172
|
+
api_res = orchestrator._api_validator.validate(
|
|
173
|
+
code, api_indexes, extra_whitelisted_imports=local_modules
|
|
174
|
+
)
|
|
175
|
+
arch_res = orchestrator._architecture_validator.validate(
|
|
176
|
+
code, patterns, constraints
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
violations = api_res.violations + arch_res.violations
|
|
180
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
181
|
+
warnings = [v for v in violations if v.severity == "warning"]
|
|
182
|
+
|
|
183
|
+
if violations:
|
|
184
|
+
print(f"Validation failed with {len(errors)} errors and {len(warnings)} warnings:", file=sys.stderr)
|
|
185
|
+
for v in violations:
|
|
186
|
+
print(f"- [{v.severity.upper()}] [{v.rule_name}]: {v.description}", file=sys.stderr)
|
|
187
|
+
sys.exit(2)
|
|
188
|
+
else:
|
|
189
|
+
print("Validation passed successfully! No contract violations found.")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print(f"Error running validation: {e}", file=sys.stderr)
|
|
192
|
+
sys.exit(3)
|
|
193
|
+
|
|
194
|
+
asyncio.run(run_validation())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
main()
|
vcase/main.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
# Load environment variables
|
|
7
|
+
load_dotenv(override=True)
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
|
|
11
|
+
from vcase.routers import health, proxy
|
|
12
|
+
|
|
13
|
+
app = FastAPI(
|
|
14
|
+
title="Runtime Knowledge Streaming API",
|
|
15
|
+
version="0.1.0",
|
|
16
|
+
docs_url="/docs",
|
|
17
|
+
redoc_url="/redoc",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app.include_router(health.router)
|
|
21
|
+
app.include_router(proxy.router)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
vcase/providers/base.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResolutionTier(int, Enum):
|
|
10
|
+
LOCK_FILE = 1
|
|
11
|
+
DECLARATION = 2
|
|
12
|
+
CONTAINERIZED = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Ecosystem(str, Enum):
|
|
16
|
+
PYTHON = "python"
|
|
17
|
+
NODE = "node"
|
|
18
|
+
RUST = "rust"
|
|
19
|
+
JAVA = "java"
|
|
20
|
+
GO = "go"
|
|
21
|
+
RUBY = "ruby"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SourceReliability(int, Enum):
|
|
25
|
+
OFFICIAL_DOCS = 4
|
|
26
|
+
GITHUB_RELEASE = 3
|
|
27
|
+
PACKAGE_REGISTRY = 2
|
|
28
|
+
COMMUNITY = 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class RawDependency:
|
|
33
|
+
name: str
|
|
34
|
+
version_constraint: str
|
|
35
|
+
ecosystem: Ecosystem
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ResolvedDependency:
|
|
40
|
+
name: str
|
|
41
|
+
version: str
|
|
42
|
+
ecosystem: Ecosystem
|
|
43
|
+
resolution_tier: ResolutionTier
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class DocSource:
|
|
48
|
+
url: str
|
|
49
|
+
reliability: SourceReliability
|
|
50
|
+
package: str
|
|
51
|
+
version: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ApiParameter:
|
|
56
|
+
name: str
|
|
57
|
+
type_hint: str
|
|
58
|
+
required: bool
|
|
59
|
+
description: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ApiSignature:
|
|
64
|
+
qualified_name: str
|
|
65
|
+
parameters: tuple[ApiParameter, ...]
|
|
66
|
+
return_type: str
|
|
67
|
+
available_since: str
|
|
68
|
+
deprecated: bool = False
|
|
69
|
+
deprecation_note: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ApiIndex:
|
|
74
|
+
package: str
|
|
75
|
+
version: str
|
|
76
|
+
signatures: tuple[ApiSignature, ...]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class InteractionConstraint:
|
|
81
|
+
rule_name: str
|
|
82
|
+
allowed_in: tuple[str, ...]
|
|
83
|
+
disallowed_in: tuple[str, ...]
|
|
84
|
+
description: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class TechnologyPattern:
|
|
89
|
+
technologies: tuple[str, ...]
|
|
90
|
+
description: str
|
|
91
|
+
correct_pattern: str
|
|
92
|
+
incorrect_pattern: str
|
|
93
|
+
constraint: InteractionConstraint
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ValidationViolation:
|
|
98
|
+
rule_name: str
|
|
99
|
+
description: str
|
|
100
|
+
line_number: int | None
|
|
101
|
+
severity: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ValidationResult:
|
|
106
|
+
passed: bool
|
|
107
|
+
violations: list[ValidationViolation] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EcosystemProvider(ABC):
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def detect(self, repo_path: Path) -> bool:
|
|
114
|
+
"""Return True if this provider applies to the repository at repo_path."""
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def parse_dependencies(self, repo_path: Path) -> list[RawDependency]:
|
|
118
|
+
"""Extract dependency names and version constraints from manifest files."""
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
async def resolve_versions(self, deps: list[RawDependency]) -> list[ResolvedDependency]:
|
|
122
|
+
"""Resolve exact compatible versions using registry APIs."""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
async def documentation_sources(self, package: str, version: str) -> list[DocSource]:
|
|
126
|
+
"""Return ranked documentation source URLs for the given package version."""
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def api_index(self, package: str, version: str) -> ApiIndex:
|
|
130
|
+
"""Return structured API signatures and parameters for the given package version."""
|
|
131
|
+
|
|
132
|
+
|