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.
Files changed (41) hide show
  1. vcase/__init__.py +60 -0
  2. vcase/api/__init__.py +1 -0
  3. vcase/api/github_client.py +24 -0
  4. vcase/api/npm_client.py +34 -0
  5. vcase/api/pypi_client.py +36 -0
  6. vcase/api/web_scraper.py +25 -0
  7. vcase/cli.py +198 -0
  8. vcase/main.py +21 -0
  9. vcase/providers/__init__.py +1 -0
  10. vcase/providers/base.py +132 -0
  11. vcase/providers/node_provider.py +167 -0
  12. vcase/providers/python_provider.py +218 -0
  13. vcase/routers/__init__.py +1 -0
  14. vcase/routers/health.py +17 -0
  15. vcase/routers/proxy.py +241 -0
  16. vcase/service/__init__.py +1 -0
  17. vcase/service/api_index_cache.py +280 -0
  18. vcase/service/api_validator.py +159 -0
  19. vcase/service/architecture_validator.py +136 -0
  20. vcase/service/ast_parser.py +234 -0
  21. vcase/service/contract_extractor.py +67 -0
  22. vcase/service/detector.py +133 -0
  23. vcase/service/interaction_graph.py +128 -0
  24. vcase/service/llm_validator.py +282 -0
  25. vcase/service/orchestrator.py +211 -0
  26. vcase/service/resolver.py +64 -0
  27. vcase/service/retriever.py +45 -0
  28. vcase/service/spec_builder.py +129 -0
  29. vcase/service/workspace_scanner.py +101 -0
  30. vcase/validator/__init__.py +1 -0
  31. vcase/validator/adapters/python_ast.py +166 -0
  32. vcase/validator/adapters/ts_ast.py +23 -0
  33. vcase/validator/core/ir.py +37 -0
  34. vcase/validator/core/rules_engine.py +122 -0
  35. vcase/validator/specs/api_contracts.json +12 -0
  36. vcase/validator/specs/architecture_rules.json +23 -0
  37. vcase_auto_context-0.1.0.dist-info/METADATA +82 -0
  38. vcase_auto_context-0.1.0.dist-info/RECORD +41 -0
  39. vcase_auto_context-0.1.0.dist-info/WHEEL +5 -0
  40. vcase_auto_context-0.1.0.dist-info/entry_points.txt +2 -0
  41. 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
+ ...
@@ -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
@@ -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
+
@@ -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
@@ -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
+