vcase-auto-context 0.1.0__tar.gz
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_auto_context-0.1.0/PKG-INFO +82 -0
- vcase_auto_context-0.1.0/pyproject.toml +36 -0
- vcase_auto_context-0.1.0/readme.md +64 -0
- vcase_auto_context-0.1.0/setup.cfg +4 -0
- vcase_auto_context-0.1.0/tests/test_api_validator.py +126 -0
- vcase_auto_context-0.1.0/tests/test_architecture_validator.py +96 -0
- vcase_auto_context-0.1.0/tests/test_ast_parser.py +39 -0
- vcase_auto_context-0.1.0/tests/test_detector.py +41 -0
- vcase_auto_context-0.1.0/tests/test_engine_python.py +46 -0
- vcase_auto_context-0.1.0/tests/test_llm_validator.py +69 -0
- vcase_auto_context-0.1.0/tests/test_resolver.py +66 -0
- vcase_auto_context-0.1.0/tests/test_workspace_scanner.py +106 -0
- vcase_auto_context-0.1.0/vcase/__init__.py +60 -0
- vcase_auto_context-0.1.0/vcase/api/__init__.py +1 -0
- vcase_auto_context-0.1.0/vcase/api/github_client.py +24 -0
- vcase_auto_context-0.1.0/vcase/api/npm_client.py +34 -0
- vcase_auto_context-0.1.0/vcase/api/pypi_client.py +36 -0
- vcase_auto_context-0.1.0/vcase/api/web_scraper.py +25 -0
- vcase_auto_context-0.1.0/vcase/cli.py +198 -0
- vcase_auto_context-0.1.0/vcase/main.py +21 -0
- vcase_auto_context-0.1.0/vcase/providers/__init__.py +1 -0
- vcase_auto_context-0.1.0/vcase/providers/base.py +132 -0
- vcase_auto_context-0.1.0/vcase/providers/node_provider.py +167 -0
- vcase_auto_context-0.1.0/vcase/providers/python_provider.py +218 -0
- vcase_auto_context-0.1.0/vcase/routers/__init__.py +1 -0
- vcase_auto_context-0.1.0/vcase/routers/health.py +17 -0
- vcase_auto_context-0.1.0/vcase/routers/proxy.py +241 -0
- vcase_auto_context-0.1.0/vcase/service/__init__.py +1 -0
- vcase_auto_context-0.1.0/vcase/service/api_index_cache.py +280 -0
- vcase_auto_context-0.1.0/vcase/service/api_validator.py +159 -0
- vcase_auto_context-0.1.0/vcase/service/architecture_validator.py +136 -0
- vcase_auto_context-0.1.0/vcase/service/ast_parser.py +234 -0
- vcase_auto_context-0.1.0/vcase/service/contract_extractor.py +67 -0
- vcase_auto_context-0.1.0/vcase/service/detector.py +133 -0
- vcase_auto_context-0.1.0/vcase/service/interaction_graph.py +128 -0
- vcase_auto_context-0.1.0/vcase/service/llm_validator.py +282 -0
- vcase_auto_context-0.1.0/vcase/service/orchestrator.py +211 -0
- vcase_auto_context-0.1.0/vcase/service/resolver.py +64 -0
- vcase_auto_context-0.1.0/vcase/service/retriever.py +45 -0
- vcase_auto_context-0.1.0/vcase/service/spec_builder.py +129 -0
- vcase_auto_context-0.1.0/vcase/service/workspace_scanner.py +101 -0
- vcase_auto_context-0.1.0/vcase/validator/__init__.py +1 -0
- vcase_auto_context-0.1.0/vcase/validator/adapters/python_ast.py +166 -0
- vcase_auto_context-0.1.0/vcase/validator/adapters/ts_ast.py +23 -0
- vcase_auto_context-0.1.0/vcase/validator/core/ir.py +37 -0
- vcase_auto_context-0.1.0/vcase/validator/core/rules_engine.py +122 -0
- vcase_auto_context-0.1.0/vcase/validator/specs/api_contracts.json +12 -0
- vcase_auto_context-0.1.0/vcase/validator/specs/architecture_rules.json +23 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/PKG-INFO +82 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/SOURCES.txt +52 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/dependency_links.txt +1 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/entry_points.txt +2 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/requires.txt +7 -0
- vcase_auto_context-0.1.0/vcase_auto_context.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcase-auto-context
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Version-Constrained API Specification Engine (VCASE)
|
|
5
|
+
Author-email: Developer <developer@example.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: fastapi>=0.115.0
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.30.6
|
|
13
|
+
Requires-Dist: httpx>=0.27.2
|
|
14
|
+
Requires-Dist: pydantic>=2.9.2
|
|
15
|
+
Requires-Dist: pydantic-settings>=2.5.2
|
|
16
|
+
Requires-Dist: cachetools>=5.5.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
18
|
+
|
|
19
|
+
# Version-Constrained API Specification Engine
|
|
20
|
+
|
|
21
|
+
A deterministic, version-aware knowledge streaming infrastructure for Large Language Models (LLMs).
|
|
22
|
+
|
|
23
|
+
## Why does this exist?
|
|
24
|
+
When LLMs generate code, they frequently rely on outdated training data or hallucinate API signatures for newer libraries. Traditional Retrieval-Augmented Generation (RAG) is often static and misses critical version-specific nuances or architectural constraints.
|
|
25
|
+
|
|
26
|
+
This engine replaces static RAG with a **five-layer defense system**. It dynamically extracts exact project dependencies, resolves them to specific versions, and streams deterministic API contracts and interaction rules directly into the LLM's system prompt.
|
|
27
|
+
|
|
28
|
+
## Core Features
|
|
29
|
+
- **Zero-Hallucination API Contracts:** Injects exact function signatures based on the specific version of the library you are using.
|
|
30
|
+
- **Architectural Guardrails:** Enforces complex interaction patterns (e.g., "Temporal workflows cannot be executed directly inside FastAPI route handlers").
|
|
31
|
+
- **Language Agnostic Architecture:** Built on a provider interface allowing expansion beyond Python to Node, Rust, Go, etc.
|
|
32
|
+
|
|
33
|
+
## Resolution Architecture
|
|
34
|
+
|
|
35
|
+
The engine uses a strict **Tiered Fallback Strategy** to guarantee dependency accuracy before building the specification:
|
|
36
|
+
|
|
37
|
+
1. **Tier 1: Lock Files (Highest Priority)**
|
|
38
|
+
* Parses deterministic manifests (e.g., `poetry.lock`) to extract exact, pinned versions.
|
|
39
|
+
2. **Tier 2: Declaration Files**
|
|
40
|
+
* Parses abstract declarations (e.g., `pyproject.toml`, `requirements.txt`).
|
|
41
|
+
* Connects to ecosystem registries (like PyPI) to resolve loose constraints (like `^1.2.0`) to the latest compatible version.
|
|
42
|
+
3. **Tier 3: Containerized Introspection (Future)**
|
|
43
|
+
* Sandboxed subprocess execution (`pip freeze`, `npm ls`) for deeply nested or hidden dependencies.
|
|
44
|
+
|
|
45
|
+
## Component Flow
|
|
46
|
+
|
|
47
|
+
1. **`TechnologyDetector`**: Scans the repository or user prompt against a curated list of ~180 production-grade technologies to identify the stack.
|
|
48
|
+
2. **`DependencyResolver`**: Orchestrates the tiered fallback logic across all available Ecosystem Providers.
|
|
49
|
+
3. **`EcosystemProvider`** (e.g., `PythonProvider`): Parses the specific manifest syntax of the language.
|
|
50
|
+
4. **`PypiClient`**: Makes async, defensive network requests to fetch current metadata and documentation sources.
|
|
51
|
+
5. **`SpecBuilder`**: Assembles the final, non-negotiable System Prompt block, formatting parameters, return types, and deprecation warnings.
|
|
52
|
+
|
|
53
|
+
## Usage / Integration
|
|
54
|
+
|
|
55
|
+
Currently, the engine provides internal APIs to assemble the System Prompt constraints.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Example Internal Flow (Pseudocode)
|
|
59
|
+
from src.service.resolver import DependencyResolver
|
|
60
|
+
from src.service.spec_builder import SpecBuilder
|
|
61
|
+
|
|
62
|
+
# 1. Resolve exact versions used in the project
|
|
63
|
+
resolver = DependencyResolver(providers=[PythonProvider(...)])
|
|
64
|
+
resolved_deps = await resolver.resolve(Path("./my_project"))
|
|
65
|
+
|
|
66
|
+
# 2. Fetch the metadata/signatures internally
|
|
67
|
+
# ...
|
|
68
|
+
|
|
69
|
+
# 3. Build the strict constraints for the LLM
|
|
70
|
+
builder = SpecBuilder()
|
|
71
|
+
system_instruction = builder.build_system_prompt(api_indexes, patterns)
|
|
72
|
+
|
|
73
|
+
print(system_instruction)
|
|
74
|
+
# Outputs:
|
|
75
|
+
# ## litellm 1.40.0
|
|
76
|
+
# - completion(model: str, messages: list) -> ModelResponse
|
|
77
|
+
# ⚠ PATTERN: fastapi + temporalio
|
|
78
|
+
# Rule [NO_WORKFLOW_IN_HANDLER]: Workflows must run in workers.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Contributing
|
|
82
|
+
When adding a new Ecosystem Provider (e.g., NodeProvider), you must adhere to the asynchronous contract defined in src/providers/base.py. All registry interactions must be defensive and non-blocking.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vcase-auto-context"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Developer", email = "developer@example.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "Version-Constrained API Specification Engine (VCASE)"
|
|
12
|
+
readme = "readme.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fastapi>=0.115.0",
|
|
21
|
+
"uvicorn[standard]>=0.30.6",
|
|
22
|
+
"httpx>=0.27.2",
|
|
23
|
+
"pydantic>=2.9.2",
|
|
24
|
+
"pydantic-settings>=2.5.2",
|
|
25
|
+
"cachetools>=5.5.0",
|
|
26
|
+
"python-dotenv>=1.0.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
vcase = "vcase.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["vcase", "vcase.api", "vcase.providers", "vcase.routers", "vcase.service", "vcase.validator", "vcase.validator.adapters", "vcase.validator.core", "vcase.validator.specs"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
vcase = ["validator/specs/*.json"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Version-Constrained API Specification Engine
|
|
2
|
+
|
|
3
|
+
A deterministic, version-aware knowledge streaming infrastructure for Large Language Models (LLMs).
|
|
4
|
+
|
|
5
|
+
## Why does this exist?
|
|
6
|
+
When LLMs generate code, they frequently rely on outdated training data or hallucinate API signatures for newer libraries. Traditional Retrieval-Augmented Generation (RAG) is often static and misses critical version-specific nuances or architectural constraints.
|
|
7
|
+
|
|
8
|
+
This engine replaces static RAG with a **five-layer defense system**. It dynamically extracts exact project dependencies, resolves them to specific versions, and streams deterministic API contracts and interaction rules directly into the LLM's system prompt.
|
|
9
|
+
|
|
10
|
+
## Core Features
|
|
11
|
+
- **Zero-Hallucination API Contracts:** Injects exact function signatures based on the specific version of the library you are using.
|
|
12
|
+
- **Architectural Guardrails:** Enforces complex interaction patterns (e.g., "Temporal workflows cannot be executed directly inside FastAPI route handlers").
|
|
13
|
+
- **Language Agnostic Architecture:** Built on a provider interface allowing expansion beyond Python to Node, Rust, Go, etc.
|
|
14
|
+
|
|
15
|
+
## Resolution Architecture
|
|
16
|
+
|
|
17
|
+
The engine uses a strict **Tiered Fallback Strategy** to guarantee dependency accuracy before building the specification:
|
|
18
|
+
|
|
19
|
+
1. **Tier 1: Lock Files (Highest Priority)**
|
|
20
|
+
* Parses deterministic manifests (e.g., `poetry.lock`) to extract exact, pinned versions.
|
|
21
|
+
2. **Tier 2: Declaration Files**
|
|
22
|
+
* Parses abstract declarations (e.g., `pyproject.toml`, `requirements.txt`).
|
|
23
|
+
* Connects to ecosystem registries (like PyPI) to resolve loose constraints (like `^1.2.0`) to the latest compatible version.
|
|
24
|
+
3. **Tier 3: Containerized Introspection (Future)**
|
|
25
|
+
* Sandboxed subprocess execution (`pip freeze`, `npm ls`) for deeply nested or hidden dependencies.
|
|
26
|
+
|
|
27
|
+
## Component Flow
|
|
28
|
+
|
|
29
|
+
1. **`TechnologyDetector`**: Scans the repository or user prompt against a curated list of ~180 production-grade technologies to identify the stack.
|
|
30
|
+
2. **`DependencyResolver`**: Orchestrates the tiered fallback logic across all available Ecosystem Providers.
|
|
31
|
+
3. **`EcosystemProvider`** (e.g., `PythonProvider`): Parses the specific manifest syntax of the language.
|
|
32
|
+
4. **`PypiClient`**: Makes async, defensive network requests to fetch current metadata and documentation sources.
|
|
33
|
+
5. **`SpecBuilder`**: Assembles the final, non-negotiable System Prompt block, formatting parameters, return types, and deprecation warnings.
|
|
34
|
+
|
|
35
|
+
## Usage / Integration
|
|
36
|
+
|
|
37
|
+
Currently, the engine provides internal APIs to assemble the System Prompt constraints.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# Example Internal Flow (Pseudocode)
|
|
41
|
+
from src.service.resolver import DependencyResolver
|
|
42
|
+
from src.service.spec_builder import SpecBuilder
|
|
43
|
+
|
|
44
|
+
# 1. Resolve exact versions used in the project
|
|
45
|
+
resolver = DependencyResolver(providers=[PythonProvider(...)])
|
|
46
|
+
resolved_deps = await resolver.resolve(Path("./my_project"))
|
|
47
|
+
|
|
48
|
+
# 2. Fetch the metadata/signatures internally
|
|
49
|
+
# ...
|
|
50
|
+
|
|
51
|
+
# 3. Build the strict constraints for the LLM
|
|
52
|
+
builder = SpecBuilder()
|
|
53
|
+
system_instruction = builder.build_system_prompt(api_indexes, patterns)
|
|
54
|
+
|
|
55
|
+
print(system_instruction)
|
|
56
|
+
# Outputs:
|
|
57
|
+
# ## litellm 1.40.0
|
|
58
|
+
# - completion(model: str, messages: list) -> ModelResponse
|
|
59
|
+
# ⚠ PATTERN: fastapi + temporalio
|
|
60
|
+
# Rule [NO_WORKFLOW_IN_HANDLER]: Workflows must run in workers.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Contributing
|
|
64
|
+
When adding a new Ecosystem Provider (e.g., NodeProvider), you must adhere to the asynchronous contract defined in src/providers/base.py. All registry interactions must be defensive and non-blocking.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from vcase.service.api_validator import ApiValidator
|
|
6
|
+
from vcase.providers.base import ApiIndex, ApiSignature, ApiParameter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestApiValidator:
|
|
10
|
+
|
|
11
|
+
def test_passes_when_all_calls_exist_in_index(self) -> None:
|
|
12
|
+
validator = ApiValidator()
|
|
13
|
+
code = "import litellm\nlitellm.completion(model='gpt-4o', messages=[])"
|
|
14
|
+
api_indexes = [
|
|
15
|
+
ApiIndex(
|
|
16
|
+
package="litellm",
|
|
17
|
+
version="1.40.0",
|
|
18
|
+
signatures=(
|
|
19
|
+
ApiSignature(
|
|
20
|
+
qualified_name="litellm.completion",
|
|
21
|
+
parameters=(
|
|
22
|
+
ApiParameter(name="model", type_hint="str", required=True, description=""),
|
|
23
|
+
ApiParameter(name="messages", type_hint="list", required=True, description="")
|
|
24
|
+
),
|
|
25
|
+
return_type="ModelResponse",
|
|
26
|
+
available_since="",
|
|
27
|
+
deprecated=False
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
result = validator.validate(code, api_indexes)
|
|
33
|
+
assert result.passed is True
|
|
34
|
+
assert len(result.violations) == 0
|
|
35
|
+
|
|
36
|
+
def test_flags_call_not_present_in_api_index(self) -> None:
|
|
37
|
+
validator = ApiValidator()
|
|
38
|
+
code = "import litellm\nlitellm.non_existent(model='gpt-5-mini')"
|
|
39
|
+
api_indexes = [
|
|
40
|
+
ApiIndex(
|
|
41
|
+
package="litellm",
|
|
42
|
+
version="1.40.0",
|
|
43
|
+
signatures=()
|
|
44
|
+
)
|
|
45
|
+
]
|
|
46
|
+
result = validator.validate(code, api_indexes)
|
|
47
|
+
assert result.passed is False
|
|
48
|
+
assert any(v.rule_name == "api-unknown" for v in result.violations)
|
|
49
|
+
|
|
50
|
+
def test_flags_deprecated_api_usage(self) -> None:
|
|
51
|
+
validator = ApiValidator()
|
|
52
|
+
code = "import litellm\nlitellm.completion(model='gpt-4o')"
|
|
53
|
+
api_indexes = [
|
|
54
|
+
ApiIndex(
|
|
55
|
+
package="litellm",
|
|
56
|
+
version="1.40.0",
|
|
57
|
+
signatures=(
|
|
58
|
+
ApiSignature(
|
|
59
|
+
qualified_name="litellm.completion",
|
|
60
|
+
parameters=(ApiParameter(name="model", type_hint="str", required=True, description=""),),
|
|
61
|
+
return_type="ModelResponse",
|
|
62
|
+
available_since="",
|
|
63
|
+
deprecated=True,
|
|
64
|
+
deprecation_note="Use acompletion instead."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
result = validator.validate(code, api_indexes)
|
|
70
|
+
assert result.passed is True
|
|
71
|
+
assert any(v.rule_name == "api-deprecated" for v in result.violations)
|
|
72
|
+
|
|
73
|
+
def test_returns_empty_violations_for_empty_code(self) -> None:
|
|
74
|
+
validator = ApiValidator()
|
|
75
|
+
result = validator.validate("", [])
|
|
76
|
+
assert result.passed is True
|
|
77
|
+
assert len(result.violations) == 0
|
|
78
|
+
|
|
79
|
+
def test_passes_with_arbitrary_kwargs_when_signature_has_kwargs(self) -> None:
|
|
80
|
+
validator = ApiValidator()
|
|
81
|
+
code = "import litellm\nlitellm.completion(model='gpt-4o', messages=[], custom_kwarg=123)"
|
|
82
|
+
api_indexes = [
|
|
83
|
+
ApiIndex(
|
|
84
|
+
package="litellm",
|
|
85
|
+
version="1.40.0",
|
|
86
|
+
signatures=(
|
|
87
|
+
ApiSignature(
|
|
88
|
+
qualified_name="litellm.completion",
|
|
89
|
+
parameters=(
|
|
90
|
+
ApiParameter(name="model", type_hint="str", required=True, description=""),
|
|
91
|
+
ApiParameter(name="**kwargs", type_hint="Any", required=False, description=""),
|
|
92
|
+
),
|
|
93
|
+
return_type="ModelResponse",
|
|
94
|
+
available_since="",
|
|
95
|
+
deprecated=False
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
result = validator.validate(code, api_indexes)
|
|
101
|
+
assert result.passed is True
|
|
102
|
+
assert len(result.violations) == 0
|
|
103
|
+
|
|
104
|
+
def test_passes_with_markdown_code_fences(self) -> None:
|
|
105
|
+
validator = ApiValidator()
|
|
106
|
+
code = "```python\nimport litellm\nlitellm.completion(model='gpt-4o')\n```"
|
|
107
|
+
api_indexes = [
|
|
108
|
+
ApiIndex(
|
|
109
|
+
package="litellm",
|
|
110
|
+
version="1.40.0",
|
|
111
|
+
signatures=(
|
|
112
|
+
ApiSignature(
|
|
113
|
+
qualified_name="litellm.completion",
|
|
114
|
+
parameters=(
|
|
115
|
+
ApiParameter(name="model", type_hint="str", required=True, description=""),
|
|
116
|
+
),
|
|
117
|
+
return_type="ModelResponse",
|
|
118
|
+
available_since="",
|
|
119
|
+
deprecated=False
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
result = validator.validate(code, api_indexes)
|
|
125
|
+
assert result.passed is True
|
|
126
|
+
assert len(result.violations) == 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from vcase.service.architecture_validator import ArchitectureValidator
|
|
6
|
+
from vcase.providers.base import InteractionConstraint, TechnologyPattern
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestArchitectureValidator:
|
|
10
|
+
|
|
11
|
+
def test_flags_temporal_workflow_called_inside_fastapi_handler(self) -> None:
|
|
12
|
+
validator = ArchitectureValidator()
|
|
13
|
+
code = """
|
|
14
|
+
import fastapi
|
|
15
|
+
import workflow
|
|
16
|
+
|
|
17
|
+
app = fastapi.FastAPI()
|
|
18
|
+
|
|
19
|
+
@app.post("/workflow")
|
|
20
|
+
async def run():
|
|
21
|
+
await workflow.run()
|
|
22
|
+
"""
|
|
23
|
+
constraint = InteractionConstraint(
|
|
24
|
+
rule_name="NO_WORKFLOW_IN_FASTAPI_ROUTE",
|
|
25
|
+
allowed_in=(),
|
|
26
|
+
disallowed_in=("app.post", "app.get"),
|
|
27
|
+
description="Temporal workflows cannot run directly in FastAPI route handlers."
|
|
28
|
+
)
|
|
29
|
+
result = validator.validate(code, [], [constraint])
|
|
30
|
+
assert result.passed is False
|
|
31
|
+
assert any(v.rule_name == "NO_WORKFLOW_IN_FASTAPI_ROUTE" for v in result.violations)
|
|
32
|
+
|
|
33
|
+
def test_flags_langfuse_used_as_sequential_log_instead_of_callback(self) -> None:
|
|
34
|
+
validator = ArchitectureValidator()
|
|
35
|
+
code = """
|
|
36
|
+
import langfuse
|
|
37
|
+
import litellm
|
|
38
|
+
|
|
39
|
+
# Sequential log call
|
|
40
|
+
langfuse.log()
|
|
41
|
+
"""
|
|
42
|
+
constraint = InteractionConstraint(
|
|
43
|
+
rule_name="NO_SEQUENTIAL_LANGFUSE_LOG",
|
|
44
|
+
allowed_in=(),
|
|
45
|
+
disallowed_in=("sequential_flow",),
|
|
46
|
+
description="Langfuse integration with LiteLLM should use callbacks."
|
|
47
|
+
)
|
|
48
|
+
result = validator.validate(code, [], [constraint])
|
|
49
|
+
assert result.passed is False
|
|
50
|
+
assert any(v.rule_name == "NO_SEQUENTIAL_LANGFUSE_LOG" for v in result.violations)
|
|
51
|
+
|
|
52
|
+
def test_flags_sqlalchemy_session_instantiated_inside_handler(self) -> None:
|
|
53
|
+
validator = ArchitectureValidator()
|
|
54
|
+
code = """
|
|
55
|
+
import fastapi
|
|
56
|
+
app = fastapi.FastAPI()
|
|
57
|
+
|
|
58
|
+
@app.post("/items")
|
|
59
|
+
def create_item():
|
|
60
|
+
db = Session()
|
|
61
|
+
"""
|
|
62
|
+
constraint = InteractionConstraint(
|
|
63
|
+
rule_name="NO_MANUAL_DB_SESSION_IN_HANDLER",
|
|
64
|
+
allowed_in=(),
|
|
65
|
+
disallowed_in=("handler_body",),
|
|
66
|
+
description="Do not manually create DB session inside handlers."
|
|
67
|
+
)
|
|
68
|
+
result = validator.validate(code, [], [constraint])
|
|
69
|
+
assert result.passed is False
|
|
70
|
+
assert any(v.rule_name == "NO_MANUAL_DB_SESSION_IN_HANDLER" for v in result.violations)
|
|
71
|
+
|
|
72
|
+
def test_passes_valid_temporal_client_usage_outside_handler(self) -> None:
|
|
73
|
+
validator = ArchitectureValidator()
|
|
74
|
+
code = """
|
|
75
|
+
import temporalio
|
|
76
|
+
import workflow
|
|
77
|
+
|
|
78
|
+
async def run_worker():
|
|
79
|
+
await workflow.run()
|
|
80
|
+
"""
|
|
81
|
+
constraint = InteractionConstraint(
|
|
82
|
+
rule_name="NO_WORKFLOW_IN_FASTAPI_ROUTE",
|
|
83
|
+
allowed_in=(),
|
|
84
|
+
disallowed_in=("app.post", "app.get"),
|
|
85
|
+
description="Temporal workflows cannot run directly in FastAPI route handlers."
|
|
86
|
+
)
|
|
87
|
+
result = validator.validate(code, [], [constraint])
|
|
88
|
+
assert result.passed is True
|
|
89
|
+
assert len(result.violations) == 0
|
|
90
|
+
|
|
91
|
+
def test_passes_when_no_constraints_apply_to_detected_stack(self) -> None:
|
|
92
|
+
validator = ArchitectureValidator()
|
|
93
|
+
code = "print('hello')"
|
|
94
|
+
result = validator.validate(code, [], [])
|
|
95
|
+
assert result.passed is True
|
|
96
|
+
assert len(result.violations) == 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from vcase.service.ast_parser import AstParser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestAstParser:
|
|
11
|
+
|
|
12
|
+
def test_parses_vararg_and_kwarg_correctly(self) -> None:
|
|
13
|
+
parser = AstParser()
|
|
14
|
+
code = """
|
|
15
|
+
def test_func(a, b=1, *args, c=2, **kwargs):
|
|
16
|
+
pass
|
|
17
|
+
"""
|
|
18
|
+
with tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="w", encoding="utf-8") as f:
|
|
19
|
+
f.write(code)
|
|
20
|
+
tmp_path = Path(f.name)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
api_index = parser.parse_file(package_name="testpkg", package_version="1.0.0", filepath=tmp_path)
|
|
24
|
+
assert api_index.package == "testpkg"
|
|
25
|
+
assert api_index.version == "1.0.0"
|
|
26
|
+
assert len(api_index.signatures) == 1
|
|
27
|
+
sig = api_index.signatures[0]
|
|
28
|
+
assert sig.qualified_name.endswith(".test_func")
|
|
29
|
+
|
|
30
|
+
param_names = [p.name for p in sig.parameters]
|
|
31
|
+
assert "a" in param_names
|
|
32
|
+
assert "*args" in param_names
|
|
33
|
+
assert "c" in param_names
|
|
34
|
+
assert "**kwargs" in param_names
|
|
35
|
+
finally:
|
|
36
|
+
try:
|
|
37
|
+
tmp_path.unlink()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from vcase.service.detector import TechnologyDetector
|
|
8
|
+
from vcase.providers.python_provider import PythonProvider
|
|
9
|
+
from vcase.providers.node_provider import NodeProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTechnologyDetector:
|
|
13
|
+
|
|
14
|
+
def test_detects_python_ecosystem_from_lock_file(self, fixture_repo_path: Path) -> None:
|
|
15
|
+
(fixture_repo_path / "poetry.lock").write_text("", encoding="utf-8")
|
|
16
|
+
provider = PythonProvider(pypi_client=None, api_index_cache=None)
|
|
17
|
+
detector = TechnologyDetector([provider])
|
|
18
|
+
assert provider in detector.detect_ecosystems(fixture_repo_path)
|
|
19
|
+
|
|
20
|
+
def test_detects_node_ecosystem_from_package_json(self, fixture_repo_path: Path) -> None:
|
|
21
|
+
(fixture_repo_path / "package.json").write_text("{}", encoding="utf-8")
|
|
22
|
+
provider = NodeProvider(npm_client=None)
|
|
23
|
+
detector = TechnologyDetector([provider])
|
|
24
|
+
assert provider in detector.detect_ecosystems(fixture_repo_path)
|
|
25
|
+
|
|
26
|
+
def test_returns_empty_list_when_no_known_ecosystem_found(self, fixture_repo_path: Path) -> None:
|
|
27
|
+
detector = TechnologyDetector([])
|
|
28
|
+
assert detector.detect_ecosystems(fixture_repo_path) == []
|
|
29
|
+
|
|
30
|
+
def test_extracts_technology_names_from_prompt(self) -> None:
|
|
31
|
+
detector = TechnologyDetector([])
|
|
32
|
+
prompt = "Create a FastAPI app that queries celery tasks and uses redis."
|
|
33
|
+
techs = detector.detect_technologies_in_prompt(prompt)
|
|
34
|
+
assert "fastapi" in techs
|
|
35
|
+
assert "celery" in techs
|
|
36
|
+
assert "redis" in techs
|
|
37
|
+
|
|
38
|
+
def test_handles_prompt_with_no_known_technologies(self) -> None:
|
|
39
|
+
detector = TechnologyDetector([])
|
|
40
|
+
prompt = "Hello, write a hello world function."
|
|
41
|
+
assert detector.detect_technologies_in_prompt(prompt) == []
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
# Fix the path before importing our internal modules
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
# 1. Import exactly the names we gave our classes
|
|
13
|
+
from vcase.api.pypi_client import PypiClient
|
|
14
|
+
from vcase.providers.python_provider import PythonProvider
|
|
15
|
+
from vcase.service.resolver import DependencyResolver
|
|
16
|
+
|
|
17
|
+
@pytest.mark.asyncio
|
|
18
|
+
async def test_engine_python():
|
|
19
|
+
logging.basicConfig(level=logging.INFO)
|
|
20
|
+
|
|
21
|
+
# 2. Assign the client to a variable!
|
|
22
|
+
# (If you don't, you can't close it later!)
|
|
23
|
+
http_client=httpx.AsyncClient()
|
|
24
|
+
api_client = PypiClient(client=http_client)
|
|
25
|
+
|
|
26
|
+
# 3. Pass the client variable into the provider
|
|
27
|
+
provider = PythonProvider(api_client)
|
|
28
|
+
|
|
29
|
+
# 4. Our class was named DependencyResolver, and it takes providers in its initialization
|
|
30
|
+
resolver = DependencyResolver(providers=[provider])
|
|
31
|
+
|
|
32
|
+
repo_path = Path(__file__).parent
|
|
33
|
+
|
|
34
|
+
# 5. Call the actual method we built: "resolve"
|
|
35
|
+
print("Executing resolution pipeline...")
|
|
36
|
+
resolved_deps = await resolver.resolve(repo_path)
|
|
37
|
+
|
|
38
|
+
# 6. We need to print them to prove it worked!
|
|
39
|
+
for dep in resolved_deps:
|
|
40
|
+
print(f"{dep.name}: {dep.version} ({dep.resolution_tier.name})")
|
|
41
|
+
|
|
42
|
+
# 7. Now we safely close the variable we created in step 2
|
|
43
|
+
await http_client.aclose()
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
asyncio.run(test_engine_python())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
import pytest
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from vcase.service.llm_validator import LlmValidator
|
|
9
|
+
from vcase.providers.base import ApiIndex, ApiSignature, ApiParameter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestLlmValidatorOpenRouter:
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
@patch.dict(os.environ, {
|
|
16
|
+
"OPENROUTER_API_KEY": "sk-or-v1-test-key",
|
|
17
|
+
"OPENAI_API_KEY": "",
|
|
18
|
+
"GEMINI_API_KEY": ""
|
|
19
|
+
})
|
|
20
|
+
async def test_validate_calls_openrouter_correctly(self) -> None:
|
|
21
|
+
validator = LlmValidator("moonshotai/kimi-k2.6")
|
|
22
|
+
|
|
23
|
+
# Mocking the response of httpx client post call
|
|
24
|
+
mock_response = httpx.Response(
|
|
25
|
+
status_code=200,
|
|
26
|
+
json={"choices": [{"message": {"content": "PASS"}}]}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# We patch httpx.AsyncClient.post
|
|
30
|
+
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
|
|
31
|
+
mock_post.return_value = mock_response
|
|
32
|
+
|
|
33
|
+
api_indexes = [
|
|
34
|
+
ApiIndex(
|
|
35
|
+
package="litellm",
|
|
36
|
+
version="1.40.0",
|
|
37
|
+
signatures=(
|
|
38
|
+
ApiSignature(
|
|
39
|
+
qualified_name="litellm.completion",
|
|
40
|
+
parameters=(
|
|
41
|
+
ApiParameter(name="model", type_hint="str", required=True, description=""),
|
|
42
|
+
),
|
|
43
|
+
return_type="ModelResponse",
|
|
44
|
+
available_since="",
|
|
45
|
+
deprecated=False
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
result = await validator.validate(
|
|
52
|
+
generated_code="import litellm\nlitellm.completion(model='gpt-4o')",
|
|
53
|
+
api_indexes=api_indexes,
|
|
54
|
+
violation_context="[api-invalid-kwargs] test violation"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert result.passed is True
|
|
58
|
+
assert len(result.violations) == 0
|
|
59
|
+
|
|
60
|
+
# Verify the call payload
|
|
61
|
+
mock_post.assert_called_once()
|
|
62
|
+
called_url = mock_post.call_args[0][0]
|
|
63
|
+
called_kwargs = mock_post.call_args[1]
|
|
64
|
+
|
|
65
|
+
assert called_url == "https://openrouter.ai/api/v1/chat/completions"
|
|
66
|
+
assert called_kwargs["headers"]["Authorization"] == "Bearer sk-or-v1-test-key"
|
|
67
|
+
assert called_kwargs["headers"]["HTTP-Referer"] == "https://github.com/smuggypusss/Version-Constrained-API-Specification-Engine"
|
|
68
|
+
assert called_kwargs["json"]["model"] == "moonshotai/kimi-k2.6"
|
|
69
|
+
assert "You are a strict code validator" in called_kwargs["json"]["messages"][0]["content"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from vcase.service.resolver import DependencyResolver
|
|
7
|
+
from vcase.providers.python_provider import PythonProvider
|
|
8
|
+
from vcase.providers.base import ResolutionTier, Ecosystem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MockPypiClient:
|
|
12
|
+
async def get_latest_version(self, package: str) -> str:
|
|
13
|
+
if package == "langfuse":
|
|
14
|
+
return "2.5.1"
|
|
15
|
+
return "1.0.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestDependencyResolver:
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
async def test_resolves_from_lock_file_as_tier_one(
|
|
22
|
+
self, fixture_repo_path: Path, sample_poetry_lock_content: str
|
|
23
|
+
) -> None:
|
|
24
|
+
(fixture_repo_path / "poetry.lock").write_text(sample_poetry_lock_content, encoding="utf-8")
|
|
25
|
+
client = MockPypiClient()
|
|
26
|
+
provider = PythonProvider(pypi_client=client, api_index_cache=None)
|
|
27
|
+
resolver = DependencyResolver(providers=[provider])
|
|
28
|
+
resolved = await resolver.resolve(fixture_repo_path)
|
|
29
|
+
assert len(resolved) == 3
|
|
30
|
+
litellm = next(d for d in resolved if d.name == "litellm")
|
|
31
|
+
assert litellm.version == "1.40.0"
|
|
32
|
+
assert litellm.resolution_tier == ResolutionTier.LOCK_FILE
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_falls_back_to_declaration_when_lock_file_missing(
|
|
36
|
+
self, fixture_repo_path: Path, sample_requirements_txt_content: str
|
|
37
|
+
) -> None:
|
|
38
|
+
(fixture_repo_path / "requirements.txt").write_text(sample_requirements_txt_content, encoding="utf-8")
|
|
39
|
+
client = MockPypiClient()
|
|
40
|
+
provider = PythonProvider(pypi_client=client, api_index_cache=None)
|
|
41
|
+
resolver = DependencyResolver(providers=[provider])
|
|
42
|
+
resolved = await resolver.resolve(fixture_repo_path)
|
|
43
|
+
assert len(resolved) == 3
|
|
44
|
+
langfuse = next(d for d in resolved if d.name == "langfuse")
|
|
45
|
+
assert langfuse.version == "2.5.1"
|
|
46
|
+
assert langfuse.resolution_tier == ResolutionTier.DECLARATION
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_logs_warning_when_no_manifest_found(self, fixture_repo_path: Path) -> None:
|
|
50
|
+
provider = PythonProvider(pypi_client=None, api_index_cache=None)
|
|
51
|
+
resolver = DependencyResolver(providers=[provider])
|
|
52
|
+
resolved = await resolver.resolve(fixture_repo_path)
|
|
53
|
+
assert resolved == []
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_normalized_output_contains_resolution_tier(
|
|
57
|
+
self, fixture_repo_path: Path, sample_poetry_lock_content: str
|
|
58
|
+
) -> None:
|
|
59
|
+
(fixture_repo_path / "poetry.lock").write_text(sample_poetry_lock_content, encoding="utf-8")
|
|
60
|
+
client = MockPypiClient()
|
|
61
|
+
provider = PythonProvider(pypi_client=client, api_index_cache=None)
|
|
62
|
+
resolver = DependencyResolver(providers=[provider])
|
|
63
|
+
resolved = await resolver.resolve(fixture_repo_path)
|
|
64
|
+
for dep in resolved:
|
|
65
|
+
assert dep.resolution_tier in (ResolutionTier.LOCK_FILE, ResolutionTier.DECLARATION)
|
|
66
|
+
assert dep.ecosystem == Ecosystem.PYTHON
|