klapcontext 0.2.0__tar.gz → 0.3.1__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.
- {klapcontext-0.2.0/src/klapcontext.egg-info → klapcontext-0.3.1}/PKG-INFO +5 -1
- {klapcontext-0.2.0 → klapcontext-0.3.1}/README.md +4 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/__init__.py +1 -1
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/compiler.py +7 -2
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/context_builder.py +25 -3
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/detector.py +40 -6
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/laravel.py +54 -10
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/code_intelligence.py +34 -3
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/system.py +22 -12
- {klapcontext-0.2.0 → klapcontext-0.3.1/src/klapcontext.egg-info}/PKG-INFO +5 -1
- {klapcontext-0.2.0 → klapcontext-0.3.1}/tests/test_klapcontext.py +41 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/LICENSE +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/pyproject.toml +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/setup.cfg +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/__init__.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/capabilities.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/impact.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/intents.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/models.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/planner.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/verification.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent/workflows.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/agent_context.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/cli.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/evidence.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/__init__.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/base.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/fastapi.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/generic_php.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/frameworks/node.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/git.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/graphify.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/portal.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/__init__.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/base.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/graphify.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/javascript_intelligence.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/python_intelligence.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/semantic.py +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext.egg-info/SOURCES.txt +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext.egg-info/dependency_links.txt +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext.egg-info/entry_points.txt +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext.egg-info/requires.txt +0 -0
- {klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: klapcontext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Make your repository understandable to humans and AI.
|
|
5
5
|
Author: Willy Cordon
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -91,6 +91,7 @@ tu `.gitignore`.
|
|
|
91
91
|
| `klap status [ruta]` | Indica si el contexto está actualizado respecto a Git. |
|
|
92
92
|
| `klap open [ruta]` | Abre el portal humano estático. |
|
|
93
93
|
| `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
|
|
94
|
+
| `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
|
|
94
95
|
| `klap --version` | Muestra la versión instalada. |
|
|
95
96
|
|
|
96
97
|
## Salidas
|
|
@@ -121,6 +122,9 @@ El workflow de GitHub Actions publica tags `v*` mediante PyPI Trusted
|
|
|
121
122
|
Publishing. Antes del primer tag, configurá el publisher de PyPI para este
|
|
122
123
|
repositorio y workflow. No se necesitan tokens de PyPI en el repositorio.
|
|
123
124
|
|
|
125
|
+
El repositorio ya incluye `.github/workflows/publish-pypi.yml`: al publicar un
|
|
126
|
+
tag `vX.Y.Z`, GitHub construye el paquete y lo publica con ese publisher.
|
|
127
|
+
|
|
124
128
|
## Licencia
|
|
125
129
|
|
|
126
130
|
Apache-2.0. Ver [LICENSE](LICENSE).
|
|
@@ -57,6 +57,7 @@ tu `.gitignore`.
|
|
|
57
57
|
| `klap status [ruta]` | Indica si el contexto está actualizado respecto a Git. |
|
|
58
58
|
| `klap open [ruta]` | Abre el portal humano estático. |
|
|
59
59
|
| `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
|
|
60
|
+
| `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
|
|
60
61
|
| `klap --version` | Muestra la versión instalada. |
|
|
61
62
|
|
|
62
63
|
## Salidas
|
|
@@ -87,6 +88,9 @@ El workflow de GitHub Actions publica tags `v*` mediante PyPI Trusted
|
|
|
87
88
|
Publishing. Antes del primer tag, configurá el publisher de PyPI para este
|
|
88
89
|
repositorio y workflow. No se necesitan tokens de PyPI en el repositorio.
|
|
89
90
|
|
|
91
|
+
El repositorio ya incluye `.github/workflows/publish-pypi.yml`: al publicar un
|
|
92
|
+
tag `vX.Y.Z`, GitHub construye el paquete y lo publica con ese publisher.
|
|
93
|
+
|
|
90
94
|
## Licencia
|
|
91
95
|
|
|
92
96
|
Apache-2.0. Ver [LICENSE](LICENSE).
|
|
@@ -27,14 +27,19 @@ def _rank(items: list[dict], terms: set[str], limit: int) -> list[dict]:
|
|
|
27
27
|
|
|
28
28
|
def compile_context(root: Path, query: str, *, detail: str = "standard", max_tokens: int = 5000) -> dict:
|
|
29
29
|
plan = ContextPlanner().plan(query, detail=detail, max_tokens=max_tokens)
|
|
30
|
-
|
|
30
|
+
cached = root / ".klap" / "context.json"
|
|
31
|
+
try:
|
|
32
|
+
context = json.loads(cached.read_text(encoding="utf-8")) if cached.exists() else build(root, {"nodes": []})
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
context = build(root, {"nodes": []})
|
|
35
|
+
model = context["system_model"]; limit = min(DETAIL_LIMITS[detail], max(1, max_tokens // 350)); terms = _terms(query, plan.area)
|
|
31
36
|
entries = _rank(model["entry_points"], terms, limit); flows = _rank(model["main_flows"], terms, limit); files = _rank(model["important_files"], terms, limit)
|
|
32
37
|
read_first = ([{"path": item.get("file"), "reason": "Punto de entrada relacionado", "relevance": item["relevance"]} for item in entries if item.get("file")] + files)[:limit]
|
|
33
38
|
result = {"intent": plan.intent, "confidence": plan.confidence, "query": query, "area": plan.area, "detail": detail, "max_tokens": max_tokens, "entry_points": entries, "flows": flows, "symbols": _rank(model["semantic_model"]["components"], terms, limit), "files": files, "dependencies": _rank(model["dependencies"], terms, limit), "tests": _rank(context["tests"], terms, limit), "read_first": read_first, "evidence": [evidence for group in entries + flows for evidence in group.get("evidence", [])][:limit], "uncertainties": model["unknowns"], "strategy": plan.strategy}
|
|
34
39
|
result["verification"] = reconcile(result["evidence"])
|
|
35
40
|
result = sanitize(result)
|
|
36
41
|
result["estimated_tokens"] = min(max_tokens, len(json.dumps(result, ensure_ascii=False)) // 4)
|
|
37
|
-
result["execution"] = {"capabilities": plan.capabilities, "providers": plan.providers, "files_selected": len(result["read_first"]), "sections_selected": len(plan.sections), "adaptive": True}
|
|
42
|
+
result["execution"] = {"capabilities": plan.capabilities, "providers": plan.providers, "files_selected": len(result["read_first"]), "sections_selected": len(plan.sections), "adaptive": True, "context_source": "cached" if cached.exists() else "generated"}
|
|
38
43
|
return result
|
|
39
44
|
|
|
40
45
|
|
|
@@ -18,6 +18,21 @@ def _nodes(graph: dict) -> list[dict]:
|
|
|
18
18
|
return graph.get("nodes", []) if isinstance(graph.get("nodes"), list) else graph.get("graph", {}).get("nodes", [])
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _edges(graph: dict) -> list[dict]:
|
|
22
|
+
"""Graphify has emitted both `edges` and `links` across supported versions."""
|
|
23
|
+
for key in ("edges", "links"):
|
|
24
|
+
value = graph.get(key)
|
|
25
|
+
if isinstance(value, list):
|
|
26
|
+
return value
|
|
27
|
+
nested = graph.get("graph", {})
|
|
28
|
+
if isinstance(nested, dict):
|
|
29
|
+
for key in ("edges", "links"):
|
|
30
|
+
value = nested.get(key)
|
|
31
|
+
if isinstance(value, list):
|
|
32
|
+
return value
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
|
|
21
36
|
def _modules(nodes: list[dict]) -> list[dict]:
|
|
22
37
|
return [{"name": item.get("label") or item.get("name") or item.get("id"), "path": item.get("file") or item.get("path"), "kind": item.get("type", "module"), "description": None, "status": "CONFIRMED", "evidence": []} for item in nodes if (item.get("label") or item.get("name") or item.get("id")) and (item.get("file") or item.get("path"))][:100]
|
|
23
38
|
|
|
@@ -55,7 +70,14 @@ def _flows(model: dict) -> list[dict]:
|
|
|
55
70
|
seen = {entry["name"], current}
|
|
56
71
|
for _ in range(4):
|
|
57
72
|
candidates = outgoing.get(current, []) or [item for source, values in outgoing.items() if source.endswith(current) for item in values]
|
|
58
|
-
|
|
73
|
+
candidates = [item for item in candidates if item["target"] not in seen]
|
|
74
|
+
current_class = current.rsplit("::", 1)[0].split("\\")[-1]
|
|
75
|
+
# A same-class call (for example a controller/service delegating to
|
|
76
|
+
# another method) is usually the main continuation of an HTTP flow.
|
|
77
|
+
# Preserve the other calls in the semantic model instead of guessing
|
|
78
|
+
# that they occur after it.
|
|
79
|
+
edge = next((item for item in candidates if item["target"].rsplit("::", 1)[0].split("\\")[-1] == current_class), None)
|
|
80
|
+
edge = edge or next(iter(candidates), None)
|
|
59
81
|
if not edge: break
|
|
60
82
|
target = edge["target"]; component = components.get(target, {})
|
|
61
83
|
steps.append({"name": target, "kind": edge["type"].casefold(), "path": component.get("file") or edge.get("file"), "status": edge["status"]})
|
|
@@ -86,7 +108,7 @@ def _empty_raw() -> dict:
|
|
|
86
108
|
|
|
87
109
|
|
|
88
110
|
def _semantic(root: Path, stack: dict) -> tuple[SemanticModel, dict]:
|
|
89
|
-
if "Laravel" in stack["frameworks"]:
|
|
111
|
+
if "Laravel" in stack["frameworks"] or "Lumen" in stack["frameworks"]:
|
|
90
112
|
model = LaravelAdapter().analyze(root)
|
|
91
113
|
return model, model.metadata["raw"]
|
|
92
114
|
if "FastAPI" in stack["frameworks"]:
|
|
@@ -116,6 +138,6 @@ def build(root: Path, graph: dict) -> dict:
|
|
|
116
138
|
tests = [{"path": item, "reason": "Tests detectados"} for item in ("tests", "test", "phpunit.xml", "pytest.ini") if (root / item).exists()]
|
|
117
139
|
story = " ".join(item for item in (value["text"], runtime["description"], "La aplicación está contenerizada con Docker." if "Docker" in deploy["tools"] else None) if item)
|
|
118
140
|
system = {"purpose": value, "project_story": {"text": story or None, "status": "CONFIRMED" if story else "UNKNOWN"}, "runtime": runtime, "framework": raw["framework"], "runtime_surfaces": raw["runtime_surfaces"], "routes": raw["routes"], "commands": raw["commands"], "scheduled_processes": raw["scheduled_processes"], "queue_jobs": raw["queue_jobs"], "events": raw["events"], "listeners": raw["listeners"], "components": raw["components"], "dependencies": raw["dependencies"], "semantic_model": model, "entry_points": points, "main_flows": flows, "system_interactions": _interactions(flows, systems, stores), "capabilities": _modules(_nodes(graph)), "inputs": inputs, "outputs": outputs, "external_systems": systems, "background_processes": background, "datastores": stores, "deployment": deploy, "observability": observability, "important_files": important, "start_here": [], "unknowns": [item for item, found in (("Propósito de negocio del proyecto", value["text"]), ("Plataforma de despliegue en producción", deploy["tools"])) if not found]}
|
|
119
|
-
technical = {"languages": stack["languages"], "frameworks": stack["frameworks"], "framework": raw["framework"], "dependencies": raw["dependencies"], "graph": {"nodes": len(_nodes(graph)), "edges": len(
|
|
141
|
+
technical = {"languages": stack["languages"], "frameworks": stack["frameworks"], "framework": raw["framework"], "dependencies": raw["dependencies"], "graph": {"nodes": len(_nodes(graph)), "edges": len(_edges(graph))}, "documents": [{"path": item["path"]} for item in docs], "tests": tests, "infrastructure": stack["infrastructure"]}
|
|
120
142
|
project = {"name": root.name, "type": stack["frameworks"][0] if stack["frameworks"] else None, "root": str(root.resolve()), "generated_at": datetime.now(timezone.utc).isoformat(), "git_commit": commit(root)}
|
|
121
143
|
return {"schema_version": "0.5", "project": project, "technical_model": technical, "system_model": system, "semantic_model": model, "human_context": {"purpose": None, "users": None, "important_processes": [], "notes": []}, "stack": stack, "architecture": {"style": "MVC" if "Laravel" in stack["frameworks"] else None, "status": "INFERRED" if "Laravel" in stack["frameworks"] else "UNKNOWN", "confidence": None, "evidence": []}, "entry_points": points, "modules": system["capabilities"], "flows": flows, "api": raw["routes"], "jobs": background, "datastores": stores, "external_systems": systems, "deployment": deploy, "observability": observability, "tests": tests, "evidence": [item.as_dict() for item in detector_evidence] + value["evidence"] + _analysis_evidence(raw)}
|
|
@@ -1,41 +1,75 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import fnmatch
|
|
5
|
+
import os
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
from .evidence import Evidence
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
EXCLUDED = {".git", ".klap", "graphify-out", "node_modules", "vendor", ".venv", "venv", ".test-venv", "build", "dist"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _sources(root: Path, pattern: str):
|
|
15
|
+
for directory, children, names in os.walk(root):
|
|
16
|
+
children[:] = [child for child in children if child not in EXCLUDED]
|
|
17
|
+
for name in names:
|
|
18
|
+
if fnmatch.fnmatch(name, pattern):
|
|
19
|
+
yield Path(directory) / name
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
10
23
|
evidence: list[Evidence] = []
|
|
11
|
-
stack = {"languages": [], "frameworks": [], "databases": [], "runtime": [], "infrastructure": []}
|
|
24
|
+
stack = {"languages": [], "frameworks": [], "databases": [], "runtime": [], "infrastructure": [], "package_managers": [], "project_type": "unknown"}
|
|
12
25
|
def add(bucket: str, value: str, path: str, reason: str) -> None:
|
|
13
26
|
if value not in stack[bucket]: stack[bucket].append(value)
|
|
14
27
|
evidence.append(Evidence("file", path, reason, "CONFIRMED", 1.0))
|
|
15
28
|
composer = root / "composer.json"
|
|
16
29
|
if composer.exists():
|
|
17
30
|
add("languages", "PHP", "composer.json", "Composer manifest detected")
|
|
31
|
+
stack["runtime"].append("PHP"); stack["package_managers"].append("composer")
|
|
18
32
|
try: deps = {**json.loads(composer.read_text(encoding="utf-8")).get("require", {}), **json.loads(composer.read_text(encoding="utf-8")).get("require-dev", {})}
|
|
19
33
|
except json.JSONDecodeError: deps = {}
|
|
20
34
|
if "laravel/framework" in deps: add("frameworks", "Laravel", "composer.json", "laravel/framework dependency detected")
|
|
21
|
-
|
|
35
|
+
if "laravel/lumen-framework" in deps:
|
|
36
|
+
add("frameworks", "Lumen", "composer.json", "laravel/lumen-framework dependency detected")
|
|
37
|
+
stack["project_type"] = "backend-api"
|
|
38
|
+
elif any(_sources(root, "*.php")):
|
|
22
39
|
# A standalone PHP repository has no Composer manifest to identify it,
|
|
23
40
|
# but language analysis can still provide a useful generic model.
|
|
24
|
-
first_php = next(root
|
|
41
|
+
first_php = next(_sources(root, "*.php"))
|
|
25
42
|
add("languages", "PHP", str(first_php.relative_to(root)).replace("\\", "/"), "PHP source file detected")
|
|
26
43
|
package = root / "package.json"
|
|
27
44
|
if package.exists():
|
|
28
45
|
add("languages", "JavaScript", "package.json", "npm manifest detected")
|
|
46
|
+
stack["runtime"].append("Node.js"); stack["package_managers"].append("npm")
|
|
29
47
|
try: deps = {**json.loads(package.read_text(encoding="utf-8")).get("dependencies", {}), **json.loads(package.read_text(encoding="utf-8")).get("devDependencies", {})}
|
|
30
48
|
except json.JSONDecodeError: deps = {}
|
|
31
49
|
for name, label in (("next", "Next.js"), ("vue", "Vue"), ("react", "React"), ("express", "Express"), ("@nestjs/core", "NestJS")):
|
|
32
50
|
if name in deps: add("frameworks", label, "package.json", f"{name} dependency detected")
|
|
51
|
+
if any(_sources(root, "*.ts")) or any(_sources(root, "*.tsx")): add("languages", "TypeScript", "package.json", "TypeScript source detected")
|
|
33
52
|
if (root / "pyproject.toml").exists() or (root / "requirements.txt").exists():
|
|
34
53
|
python_manifest = root / "pyproject.toml" if (root / "pyproject.toml").exists() else root / "requirements.txt"
|
|
35
54
|
add("languages", "Python", python_manifest.name, "Python manifest detected")
|
|
55
|
+
stack["runtime"].append("Python"); stack["package_managers"].append("pip")
|
|
36
56
|
if "fastapi" in python_manifest.read_text(encoding="utf-8", errors="ignore").casefold(): add("frameworks", "FastAPI", python_manifest.name, "FastAPI dependency detected")
|
|
37
57
|
if (root / "manage.py").exists(): add("frameworks", "Django", "manage.py", "Django entry point detected")
|
|
38
|
-
if list(root.glob("*.csproj")):
|
|
58
|
+
if list(root.glob("*.csproj")):
|
|
59
|
+
add("languages", "C#", next(root.glob("*.csproj")).name, ".NET project detected"); stack["runtime"].append(".NET"); stack["package_managers"].append("NuGet")
|
|
60
|
+
if (root / "Program.cs").exists(): add("frameworks", ".NET Web API", "Program.cs", "ASP.NET entry point detected")
|
|
61
|
+
if (root / "go.mod").exists():
|
|
62
|
+
add("languages", "Go", "go.mod", "Go module detected"); stack["runtime"].append("Go"); stack["package_managers"].append("go modules")
|
|
63
|
+
if (root / "Cargo.toml").exists():
|
|
64
|
+
add("languages", "Rust", "Cargo.toml", "Rust manifest detected"); stack["runtime"].append("Rust"); stack["package_managers"].append("cargo")
|
|
65
|
+
if (root / "pom.xml").exists() or (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
|
|
66
|
+
manifest = "pom.xml" if (root / "pom.xml").exists() else "build.gradle" if (root / "build.gradle").exists() else "build.gradle.kts"
|
|
67
|
+
add("languages", "Java", manifest, "Java build manifest detected"); stack["runtime"].append("JVM"); stack["package_managers"].append("maven" if manifest == "pom.xml" else "gradle")
|
|
68
|
+
# Keep generic analysis useful for small repositories without a manifest.
|
|
69
|
+
for suffix, language in (("*.py", "Python"), ("*.js", "JavaScript"), ("*.ts", "TypeScript"), ("*.java", "Java"), ("*.go", "Go"), ("*.rs", "Rust")):
|
|
70
|
+
if language not in stack["languages"] and any(_sources(root, suffix)):
|
|
71
|
+
first = next(_sources(root, suffix)); add("languages", language, first.relative_to(root).as_posix(), f"{language} source file detected")
|
|
72
|
+
if any(name in stack["frameworks"] for name in ("Laravel", "Lumen", "FastAPI", "Express", "NestJS", ".NET Web API")): stack["project_type"] = "backend-api"
|
|
39
73
|
if (root / "Dockerfile").exists(): add("infrastructure", "Docker", "Dockerfile", "Dockerfile detected")
|
|
40
74
|
if list(root.glob("docker-compose*")) or list(root.glob("compose*")): add("infrastructure", "Docker Compose", "docker-compose", "Compose configuration detected")
|
|
41
75
|
if (root / ".github" / "workflows").exists(): add("infrastructure", "GitHub Actions", ".github/workflows", "GitHub Actions workflows detected")
|
|
@@ -45,8 +79,8 @@ def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
|
45
79
|
def detect_components(root: Path) -> list[dict]:
|
|
46
80
|
"""Find independent project boundaries without assuming a monorepo layout."""
|
|
47
81
|
result = []
|
|
48
|
-
for
|
|
49
|
-
|
|
82
|
+
manifests = [path for pattern in ("composer.json", "package.json", "pyproject.toml", "requirements.txt") for path in _sources(root, pattern)]
|
|
83
|
+
for manifest in manifests:
|
|
50
84
|
directory = manifest.parent
|
|
51
85
|
if directory == root: continue
|
|
52
86
|
stack, _ = detect_project(directory)
|
|
@@ -28,7 +28,7 @@ def _e(root: Path, path: Path, offset: int, reason: str, *, symbol: str | None =
|
|
|
28
28
|
|
|
29
29
|
def _category(package: str) -> str:
|
|
30
30
|
package = package.lower()
|
|
31
|
-
if package
|
|
31
|
+
if package in {"laravel/framework", "laravel/lumen-framework"}: return "framework"
|
|
32
32
|
if any(token in package for token in ("mysql", "postgres", "mongodb", "redis", "doctrine/dbal")): return "database"
|
|
33
33
|
if any(token in package for token in ("queue", "horizon", "rabbit", "kafka")): return "queue"
|
|
34
34
|
if any(token in package for token in ("jwt", "sanctum", "passport", "auth")): return "authentication"
|
|
@@ -42,18 +42,48 @@ def _category(package: str) -> str:
|
|
|
42
42
|
def _composer(root: Path) -> tuple[list[dict], dict]:
|
|
43
43
|
composer = root / "composer.json"
|
|
44
44
|
if not composer.exists():
|
|
45
|
-
return [], {"
|
|
45
|
+
return [], {"framework": None, "package": None, "version": None, "php": None}
|
|
46
46
|
try:
|
|
47
47
|
data = json.loads(_read(composer))
|
|
48
48
|
except json.JSONDecodeError:
|
|
49
|
-
return [], {"
|
|
49
|
+
return [], {"framework": None, "package": None, "version": None, "php": None}
|
|
50
50
|
dependencies = []
|
|
51
51
|
for scope, key in (("runtime", "require"), ("development", "require-dev")):
|
|
52
52
|
for package, version in data.get(key, {}).items():
|
|
53
53
|
evidence = Evidence("manifest", "composer.json", f"Dependencia Composer ({scope})", "CONFIRMED", 1.0, symbol=package).as_dict()
|
|
54
54
|
dependencies.append({"package": package, "version": version, "scope": scope, "category": _category(package), "status": "CONFIRMED", "evidence": [evidence]})
|
|
55
55
|
requirements = data.get("require", {})
|
|
56
|
-
|
|
56
|
+
package = "laravel/lumen-framework" if "laravel/lumen-framework" in requirements else "laravel/framework" if "laravel/framework" in requirements else None
|
|
57
|
+
return dependencies, {"framework": "Lumen" if package == "laravel/lumen-framework" else "Laravel" if package else None, "package": package, "version": requirements.get(package) if package else None, "php": requirements.get("php")}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _group_spans(text: str) -> list[tuple[int, int, str, list[str]]]:
|
|
61
|
+
"""Return Lumen/Laravel route-group bodies with their inherited metadata."""
|
|
62
|
+
pattern = re.compile(r"(?:\$router|\$app|Route)->group\s*\(\s*\[(.*?)\]\s*,\s*function\s*\([^)]*\)\s*(?:use\s*\([^)]*\)\s*)?\{", re.S)
|
|
63
|
+
groups = []
|
|
64
|
+
for match in pattern.finditer(text):
|
|
65
|
+
opening = match.end() - 1
|
|
66
|
+
depth, closing = 0, None
|
|
67
|
+
for index in range(opening, len(text)):
|
|
68
|
+
if text[index] == "{": depth += 1
|
|
69
|
+
elif text[index] == "}":
|
|
70
|
+
depth -= 1
|
|
71
|
+
if depth == 0:
|
|
72
|
+
closing = index
|
|
73
|
+
break
|
|
74
|
+
if closing is None:
|
|
75
|
+
continue
|
|
76
|
+
options = match.group(1)
|
|
77
|
+
prefix_match = re.search(r"['\"]prefix['\"]\s*=>\s*['\"]([^'\"]*)['\"]", options)
|
|
78
|
+
middleware_match = re.search(r"['\"]middleware['\"]\s*=>\s*(\[[^\]]*\]|['\"][^'\"]*['\"])", options, re.S)
|
|
79
|
+
middleware = re.findall(r"['\"]([^'\"]+)['\"]", middleware_match.group(1)) if middleware_match else []
|
|
80
|
+
groups.append((opening, closing, prefix_match.group(1) if prefix_match else "", middleware))
|
|
81
|
+
return groups
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _comment_spans(text: str) -> list[tuple[int, int]]:
|
|
85
|
+
"""Offsets covered by PHP line or block comments, preserving source lines."""
|
|
86
|
+
return [(match.start(), match.end()) for match in re.finditer(r"//[^\n]*|/\*.*?\*/", text, re.S)]
|
|
57
87
|
|
|
58
88
|
|
|
59
89
|
def _routes(root: Path) -> tuple[list[dict], list[dict]]:
|
|
@@ -61,16 +91,28 @@ def _routes(root: Path) -> tuple[list[dict], list[dict]]:
|
|
|
61
91
|
directory = root / "routes"
|
|
62
92
|
if not directory.is_dir():
|
|
63
93
|
return routes, scheduled
|
|
64
|
-
route_pattern = re.compile(r"Route
|
|
94
|
+
route_pattern = re.compile(r"(?:Route|\$app|\$router)(?:::|->)(get|post|put|patch|delete|options|any)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*(.+?)\);", re.S | re.I)
|
|
65
95
|
resource_pattern = re.compile(r"Route::(?:api)?resource\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z_]\w*(?:\\[A-Za-z_]\w*)*)::class", re.I)
|
|
66
96
|
schedule_pattern = re.compile(r"Schedule::(?:command|job)\s*\(\s*['\"]([^'\"]+)['\"]\s*\).*?->([A-Za-z]\w*)\s*\(", re.S)
|
|
67
97
|
for file in directory.glob("*.php"):
|
|
68
98
|
text = _read(file)
|
|
99
|
+
groups = _group_spans(text)
|
|
100
|
+
comments = _comment_spans(text)
|
|
69
101
|
for match in route_pattern.finditer(text):
|
|
102
|
+
if any(start <= match.start() < end for start, end in comments):
|
|
103
|
+
continue
|
|
70
104
|
method, uri, target = match.groups()
|
|
71
105
|
handler = _symbol(target)
|
|
72
|
-
|
|
73
|
-
|
|
106
|
+
if not handler:
|
|
107
|
+
callback = re.search(r"['\"]([A-Za-z_]\w*)@([A-Za-z_]\w*)['\"]", target)
|
|
108
|
+
handler = f"{callback.group(1)}::{callback.group(2)}" if callback else None
|
|
109
|
+
active = [group for group in groups if group[0] < match.start() < group[1]]
|
|
110
|
+
prefix = "/".join(group[2].strip("/") for group in active if group[2])
|
|
111
|
+
middleware = [name for group in active for name in group[3]]
|
|
112
|
+
full_uri = "/" + "/".join(part for part in (prefix, uri.strip("/")) if part)
|
|
113
|
+
name = f"{method.upper()} {full_uri}"
|
|
114
|
+
framework = "Lumen" if "lumen-framework" in _read(root / "composer.json") else "Laravel"
|
|
115
|
+
routes.append({"type": "http", "method": method.upper(), "uri": full_uri, "name": name, "path": str(file.relative_to(root)), "source": str(file.relative_to(root)), "line": text.count("\n", 0, match.start()) + 1, "target": handler, "handler": handler, "middleware": middleware, "status": "CONFIRMED", "evidence": _e(root, file, match.start(), f"Declaración de ruta {framework}", symbol=name)})
|
|
74
116
|
for match in resource_pattern.finditer(text):
|
|
75
117
|
resource, controller = match.groups()
|
|
76
118
|
name = f"RESOURCE /{resource.lstrip('/')}"
|
|
@@ -137,7 +179,9 @@ def analyze(root: Path) -> dict:
|
|
|
137
179
|
for kind, label, values in (("http", "HTTP", routes), ("cli", "CLI", commands), ("scheduler", "Scheduler", scheduled), ("queue", "Queue", jobs), ("event", "Eventos", events), ("listener", "Listeners", listeners)):
|
|
138
180
|
if values:
|
|
139
181
|
surfaces.append({"type": kind, "label": label, "count": len(values), "items": values})
|
|
140
|
-
|
|
182
|
+
framework = versions["framework"] or "Laravel"
|
|
183
|
+
package = versions["package"] or "laravel/framework"
|
|
184
|
+
return {"framework": {"name": framework, "version": versions["version"], "php_version": versions["php"], "status": "CONFIRMED", "evidence": [Evidence("manifest", "composer.json", f"{package} declarado", "CONFIRMED", 1.0, symbol=package).as_dict()]}, "routes": routes, "commands": commands, "scheduled_processes": scheduled, "queue_jobs": jobs, "events": events, "listeners": listeners, "components": components, "dependencies": dependencies, "runtime_surfaces": surfaces, "important_files": important}
|
|
141
185
|
|
|
142
186
|
|
|
143
187
|
class LaravelAdapter:
|
|
@@ -154,7 +198,7 @@ class LaravelAdapter:
|
|
|
154
198
|
entries = []
|
|
155
199
|
transitions = []
|
|
156
200
|
for route in raw["routes"]:
|
|
157
|
-
entries.append(EntryPoint("HTTP", route["name"], route.get("target"), "HTTP", route.get("method"), route.get("uri"), route["path"], route["line"], "
|
|
201
|
+
entries.append(EntryPoint("HTTP", route["name"], route.get("target"), "HTTP", route.get("method"), route.get("uri"), route["path"], route["line"], raw["framework"]["name"], route["status"], route["evidence"]))
|
|
158
202
|
if route.get("target"):
|
|
159
203
|
transitions.append(ExecutionTransition(route["name"], route["target"], "HTTP_ENTRY", route["path"], route["line"], route["status"], "laravel", route["evidence"]))
|
|
160
204
|
for command in raw["commands"]:
|
|
@@ -184,4 +228,4 @@ class LaravelAdapter:
|
|
|
184
228
|
events = [Event(item["name"], item["path"], item["status"], item["evidence"]) for item in raw["events"]]
|
|
185
229
|
handlers = [EventHandler(item["name"], file=item["path"], status=item["status"], evidence=item["evidence"]) for item in raw["listeners"]]
|
|
186
230
|
dependencies = [Dependency(item["package"], item["version"], item["category"], item["status"], item["evidence"]) for item in raw["dependencies"]]
|
|
187
|
-
return SemanticModel("PHP", "
|
|
231
|
+
return SemanticModel("PHP", raw["framework"]["name"], "FRAMEWORK", components, entries, transitions, background_tasks=background, events=events, event_handlers=handlers, dependencies=dependencies, metadata={"raw": raw})
|
|
@@ -3,12 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
6
8
|
from dataclasses import asdict, dataclass, field
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
EXCLUDED = {".git", ".klap", "vendor", "node_modules", "build", "dist", ".venv", "venv", ".test-venv"}
|
|
11
|
-
INDEX_VERSION =
|
|
13
|
+
INDEX_VERSION = 4
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@dataclass(frozen=True)
|
|
@@ -80,7 +82,11 @@ class PhpCodeIntelligenceProvider:
|
|
|
80
82
|
self._index: dict | None = None
|
|
81
83
|
|
|
82
84
|
def _files(self) -> list[Path]:
|
|
83
|
-
|
|
85
|
+
files = []
|
|
86
|
+
for directory, children, names in os.walk(self.root):
|
|
87
|
+
children[:] = [child for child in children if child not in EXCLUDED]
|
|
88
|
+
files.extend(Path(directory) / name for name in names if name.endswith(".php"))
|
|
89
|
+
return sorted(files)
|
|
84
90
|
|
|
85
91
|
def _fingerprint(self, files: list[Path]) -> str:
|
|
86
92
|
value = "|".join(f"{path.relative_to(self.root)}:{path.stat().st_mtime_ns}:{path.stat().st_size}" for path in files)
|
|
@@ -133,6 +139,7 @@ class PhpCodeIntelligenceProvider:
|
|
|
133
139
|
for target in [part.strip() for part in _text(child, source).replace("implements", "").split(",")]:
|
|
134
140
|
relations.append(asdict(CodeRelation(qualified, target, "IMPLEMENTS", relative, _line(source, child.start_byte), "CONFIRMED")))
|
|
135
141
|
properties = self._properties(declaration, source)
|
|
142
|
+
properties.update(self._constructor_properties(declaration, source))
|
|
136
143
|
for method in (node for node in _descendants(declaration) if node.type == "method_declaration"):
|
|
137
144
|
method_name = _field_text(method, "name", source)
|
|
138
145
|
if not method_name:
|
|
@@ -161,6 +168,28 @@ class PhpCodeIntelligenceProvider:
|
|
|
161
168
|
values[variable.lstrip("$")] = type_name.split("\\")[-1]
|
|
162
169
|
return values
|
|
163
170
|
|
|
171
|
+
def _constructor_properties(self, declaration, source: bytes) -> dict[str, str]:
|
|
172
|
+
"""Map classic constructor injection (``$this->x = $x``) to its type.
|
|
173
|
+
|
|
174
|
+
Lumen and older Laravel applications commonly use this form instead of
|
|
175
|
+
PHP 8 property promotion. It is deliberately conservative: only a
|
|
176
|
+
typed constructor parameter assigned directly to a ``$this`` property
|
|
177
|
+
is resolved.
|
|
178
|
+
"""
|
|
179
|
+
text = _text(declaration, source)
|
|
180
|
+
constructor = re.search(r"function\s+__construct\s*\((.*?)\)\s*\{(.*?)\}", text, re.S)
|
|
181
|
+
if not constructor:
|
|
182
|
+
return {}
|
|
183
|
+
parameters = {
|
|
184
|
+
variable: type_name.split("\\")[-1]
|
|
185
|
+
for type_name, variable in re.findall(r"(?:public|protected|private)?\s*([A-Za-z_]\w*(?:\\[A-Za-z_]\w*)*)\s+\$(\w+)", constructor.group(1))
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
property_name: parameters[variable]
|
|
189
|
+
for property_name, variable in re.findall(r"\$this->(\w+)\s*=\s*\$(\w+)\s*;", constructor.group(2))
|
|
190
|
+
if variable in parameters
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
def _calls(self, method, source: bytes, source_id: str, file: str, properties: dict[str, str]) -> list[dict]:
|
|
165
194
|
relations = []
|
|
166
195
|
for node in _descendants(method):
|
|
@@ -171,7 +200,9 @@ class PhpCodeIntelligenceProvider:
|
|
|
171
200
|
name = _field_text(node, "name", source)
|
|
172
201
|
object_node = node.child_by_field_name("object")
|
|
173
202
|
object_text = _text(object_node, source) if object_node else ""
|
|
174
|
-
if object_text
|
|
203
|
+
if object_text == "$this":
|
|
204
|
+
target = f"{source_id.rsplit('::', 1)[0]}::{name}"
|
|
205
|
+
elif object_text.startswith("$this->"):
|
|
175
206
|
service = object_text.split("->", 1)[1]
|
|
176
207
|
target = f"{properties.get(service, service)}::{name}"
|
|
177
208
|
elif object_text.startswith("$"):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ from .evidence import Evidence
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
TEXT_LIMIT = 180_000
|
|
13
|
+
EXCLUDED = {".git", ".klap", "graphify-out", "node_modules", "vendor", ".venv", "venv", ".pytest_cache", ".test-venv", "build", "dist"}
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def _read(path: Path) -> str:
|
|
@@ -16,6 +18,16 @@ def _read(path: Path) -> str:
|
|
|
16
18
|
except OSError: return ""
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
def _source_files(root: Path, suffixes: set[str]):
|
|
22
|
+
"""Walk source files without first descending into dependency trees."""
|
|
23
|
+
for directory, children, names in os.walk(root):
|
|
24
|
+
children[:] = [child for child in children if child not in EXCLUDED]
|
|
25
|
+
for name in names:
|
|
26
|
+
path = Path(directory) / name
|
|
27
|
+
if path.suffix.lower() in suffixes:
|
|
28
|
+
yield path
|
|
29
|
+
|
|
30
|
+
|
|
19
31
|
def _ev(path: str, reason: str, status: str = "CONFIRMED", confidence: float = 1.0) -> dict:
|
|
20
32
|
return Evidence("file", path, reason, status, confidence).as_dict()
|
|
21
33
|
|
|
@@ -74,16 +86,15 @@ def entry_points(root: Path) -> tuple[list[dict], list[dict]]:
|
|
|
74
86
|
point=add("scheduled", command, rel, schedule=frequency, reason="Laravel scheduler declaration")
|
|
75
87
|
background.append({"name":command,"type":"scheduled_command","schedule":frequency,"path":rel,"status":"CONFIRMED","evidence":point["evidence"]})
|
|
76
88
|
# Python HTTP decorators and CLI bootstrap.
|
|
77
|
-
for path in root.
|
|
78
|
-
if
|
|
89
|
+
for path in _source_files(root, {".py"}):
|
|
90
|
+
if path.name == "system.py": continue
|
|
79
91
|
text=_read(path); rel=str(path.relative_to(root))
|
|
80
92
|
for method, uri, function in re.findall(r"@\w+\.(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]+)['\"]\s*\)\s*\n\s*(?:async\s+)?def\s+(\w+)", text, re.I):
|
|
81
93
|
add("http", f"{method.upper()} {uri}", rel, function, reason="Python web route decorator")
|
|
82
94
|
if "if __name__ == \"__main__\"" in text or "if __name__ == '__main__'" in text:
|
|
83
95
|
add("cli", path.stem, rel, reason="Python executable module")
|
|
84
96
|
# Express route declarations.
|
|
85
|
-
for path in root.
|
|
86
|
-
if any(part in {"node_modules", ".git", ".klap", "graphify-out", ".test-venv", "build", "dist"} for part in path.parts): continue
|
|
97
|
+
for path in _source_files(root, {".js"}):
|
|
87
98
|
rel=str(path.relative_to(root)); text=_read(path)
|
|
88
99
|
for method, uri in re.findall(r"\.(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]+)", text, re.I): add("http", f"{method.upper()} {uri}", rel, reason="JavaScript HTTP route declaration")
|
|
89
100
|
return points[:50], background[:30]
|
|
@@ -93,9 +104,9 @@ def external_systems(root: Path) -> tuple[list[dict], list[dict]]:
|
|
|
93
104
|
found, stores = [], []
|
|
94
105
|
patterns = [(r"\bredis\b", "Redis", "redis"), (r"\brabbitmq\b|\bamqp\b", "RabbitMQ", "message broker"), (r"\bkafka\b", "Kafka", "message broker"), (r"\bsentry\b", "Sentry", "error tracking"), (r"\bsmtp\b|\bmailgun\b", "SMTP", "email"), (r"\bsftp\b", "SFTP", "sftp"), (r"\bftp\b", "FTP", "ftp")]
|
|
95
106
|
store_patterns = [(r"\bmysql\b", "MySQL"), (r"\bpostgres(?:ql)?\b", "PostgreSQL"), (r"\bsqlsrv\b|sql server", "SQL Server"), (r"\bsqlite\b", "SQLite"), (r"\bmongodb\b", "MongoDB")]
|
|
96
|
-
candidates=
|
|
107
|
+
candidates = list(_source_files(root, {".json", ".toml", ".yml", ".yaml", ".php", ".py", ".js", ".ts", ".env"}))
|
|
97
108
|
for path in candidates[:800]:
|
|
98
|
-
if
|
|
109
|
+
if "tests" in path.relative_to(root).parts or "frameworks" in path.relative_to(root).parts or path.name in {"system.py", "portal.py", "pyproject.toml"}: continue
|
|
99
110
|
text=_read(path); rel=str(path.relative_to(root))
|
|
100
111
|
for pattern,name,kind in patterns:
|
|
101
112
|
if re.search(pattern, text, re.I) and not any(x["name"] == name for x in found): found.append({"name":name,"type":kind,"purpose":None,"used_by":[],"configuration":[rel],"status":"CONFIRMED","evidence":[_ev(rel, f"{name} configuration or usage detected")]})
|
|
@@ -114,10 +125,9 @@ def deployment(root: Path) -> tuple[dict, dict, list[dict]]:
|
|
|
114
125
|
for item in files:
|
|
115
126
|
text=_read(root/item["path"]) if (root/item["path"]).is_file() else ""
|
|
116
127
|
ports.extend(re.findall(r"(?:EXPOSE\s+|['\"]?)(\d{2,5})(?::\d{2,5})?", text))
|
|
117
|
-
for path in root.
|
|
118
|
-
if path.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if term in text.lower() and not any(x["name"]==name for x in observability): observability.append({"name":name,"path":rel,"status":"CONFIRMED","evidence":[_ev(rel, f"{name} reference detected")]})
|
|
128
|
+
for path in _source_files(root, {".py", ".php", ".js", ".ts", ".json", ".yml", ".yaml"}):
|
|
129
|
+
if "tests" in path.relative_to(root).parts or path.name in {"system.py", "laravel.py"}: continue
|
|
130
|
+
text=_read(path); rel=str(path.relative_to(root))
|
|
131
|
+
for term,name in (("prometheus","Prometheus"),("opentelemetry","OpenTelemetry"),("sentry","Sentry"),("/health","Health endpoint"),("/metrics","Metrics endpoint")):
|
|
132
|
+
if term in text.lower() and not any(x["name"]==name for x in observability): observability.append({"name":name,"path":rel,"status":"CONFIRMED","evidence":[_ev(rel, f"{name} reference detected")]})
|
|
123
133
|
return {"tools":tools,"ports":sorted(set(ports)),"files":files,"status":"CONFIRMED" if files else "UNKNOWN"}, {"tools":observability,"status":"CONFIRMED" if observability else "UNKNOWN"}, files
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: klapcontext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Make your repository understandable to humans and AI.
|
|
5
5
|
Author: Willy Cordon
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -91,6 +91,7 @@ tu `.gitignore`.
|
|
|
91
91
|
| `klap status [ruta]` | Indica si el contexto está actualizado respecto a Git. |
|
|
92
92
|
| `klap open [ruta]` | Abre el portal humano estático. |
|
|
93
93
|
| `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
|
|
94
|
+
| `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
|
|
94
95
|
| `klap --version` | Muestra la versión instalada. |
|
|
95
96
|
|
|
96
97
|
## Salidas
|
|
@@ -121,6 +122,9 @@ El workflow de GitHub Actions publica tags `v*` mediante PyPI Trusted
|
|
|
121
122
|
Publishing. Antes del primer tag, configurá el publisher de PyPI para este
|
|
122
123
|
repositorio y workflow. No se necesitan tokens de PyPI en el repositorio.
|
|
123
124
|
|
|
125
|
+
El repositorio ya incluye `.github/workflows/publish-pypi.yml`: al publicar un
|
|
126
|
+
tag `vX.Y.Z`, GitHub construye el paquete y lo publica con ese publisher.
|
|
127
|
+
|
|
124
128
|
## Licencia
|
|
125
129
|
|
|
126
130
|
Apache-2.0. Ver [LICENSE](LICENSE).
|
|
@@ -37,6 +37,32 @@ def test_detector_collects_confirmed_evidence(tmp_path):
|
|
|
37
37
|
assert all(item.status == "CONFIRMED" for item in evidence)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def test_detector_identifies_manifest_and_source_languages(tmp_path):
|
|
41
|
+
(tmp_path / "go.mod").write_text("module example")
|
|
42
|
+
(tmp_path / "src").mkdir(); (tmp_path / "src" / "main.ts").write_text("export {}")
|
|
43
|
+
stack, _ = detect_project(tmp_path)
|
|
44
|
+
assert {"Go", "TypeScript"} <= set(stack["languages"])
|
|
45
|
+
assert "go modules" in stack["package_managers"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_lumen_detection_and_route_adapter():
|
|
49
|
+
root = Path(__file__).parent / "fixtures" / "lumen_semantic"
|
|
50
|
+
stack, _ = detect_project(root)
|
|
51
|
+
assert "Lumen" in stack["frameworks"]
|
|
52
|
+
context = build(root, {"nodes": []})
|
|
53
|
+
route = context["system_model"]["routes"][0]
|
|
54
|
+
assert route["name"] == "POST /api/v1/call-vtex"
|
|
55
|
+
assert len(context["system_model"]["routes"]) == 1
|
|
56
|
+
assert route["middleware"] == ["apikey"]
|
|
57
|
+
assert context["system_model"]["framework"]["name"] == "Lumen"
|
|
58
|
+
flow = context["system_model"]["main_flows"][0]
|
|
59
|
+
assert [step["name"].split("\\")[-1] for step in flow["steps"]] == [
|
|
60
|
+
"POST /api/v1/call-vtex", "ProcessRequestController::callVtex",
|
|
61
|
+
"ProcessRequestService::validateRequest", "ProcessRequestService::callVtex",
|
|
62
|
+
"VtexService::searchOrderVtex", "VtexService::getOrderDetails",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
40
66
|
def test_portal_has_dashboard_and_real_views(tmp_path):
|
|
41
67
|
context = build(fixture_repo(tmp_path), {"nodes": [{"name": "UserController", "path": "app/Http/UserController.php", "type": "controller"}]})
|
|
42
68
|
page = render_portal(context, render_agent(context), ["graph.html"])
|
|
@@ -207,6 +233,21 @@ def test_context_compiler_ranks_compact_task_context():
|
|
|
207
233
|
assert minimal["execution"]["adaptive"] and minimal["execution"]["files_selected"] <= deep["execution"]["files_selected"]
|
|
208
234
|
|
|
209
235
|
|
|
236
|
+
def test_context_compiler_reuses_generated_context(tmp_path, monkeypatch):
|
|
237
|
+
root = fixture_repo(tmp_path)
|
|
238
|
+
(root / ".klap").mkdir()
|
|
239
|
+
cached = build(root, {"nodes": []})
|
|
240
|
+
(root / ".klap" / "context.json").write_text(json.dumps(cached))
|
|
241
|
+
monkeypatch.setattr("klapcontext.agent.compiler.build", lambda *_: (_ for _ in ()).throw(AssertionError("should not rebuild")))
|
|
242
|
+
result = compile_context(root, "entender sistema")
|
|
243
|
+
assert result["execution"]["context_source"] == "cached"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_graph_links_are_counted_like_edges(tmp_path):
|
|
247
|
+
context = build(fixture_repo(tmp_path), {"nodes": [], "links": [{"source": "a", "target": "b"}]})
|
|
248
|
+
assert context["technical_model"]["graph"]["edges"] == 1
|
|
249
|
+
|
|
250
|
+
|
|
210
251
|
def test_specialized_contexts_keep_the_compact_compiler_contract():
|
|
211
252
|
root = Path(__file__).parent / "fixtures" / "laravel_semantic"
|
|
212
253
|
assert change_context(root, "modificar orders")["context_type"] == "CHANGE"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{klapcontext-0.2.0 → klapcontext-0.3.1}/src/klapcontext/providers/javascript_intelligence.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|