klapcontext 0.3.2__tar.gz → 0.4.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.
- {klapcontext-0.3.2/src/klapcontext.egg-info → klapcontext-0.4.0}/PKG-INFO +9 -1
- {klapcontext-0.3.2 → klapcontext-0.4.0}/README.md +8 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/__init__.py +1 -1
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/compiler.py +5 -2
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent_context.py +6 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/cli.py +5 -2
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/context_builder.py +8 -2
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/detector.py +26 -16
- klapcontext-0.4.0/src/klapcontext/exploration.py +118 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/laravel.py +4 -1
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/portal.py +38 -6
- {klapcontext-0.3.2 → klapcontext-0.4.0/src/klapcontext.egg-info}/PKG-INFO +9 -1
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext.egg-info/SOURCES.txt +2 -0
- klapcontext-0.4.0/tests/test_exploration.py +68 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/tests/test_klapcontext.py +2 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/LICENSE +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/pyproject.toml +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/setup.cfg +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/__init__.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/capabilities.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/impact.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/intents.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/models.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/planner.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/verification.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/agent/workflows.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/evidence.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/__init__.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/base.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/fastapi.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/generic_php.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/frameworks/node.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/git.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/graphify.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/__init__.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/base.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/code_intelligence.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/graphify.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/javascript_intelligence.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/providers/python_intelligence.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/semantic.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext/system.py +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext.egg-info/dependency_links.txt +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext.egg-info/entry_points.txt +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/src/klapcontext.egg-info/requires.txt +0 -0
- {klapcontext-0.3.2 → klapcontext-0.4.0}/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.4.0
|
|
4
4
|
Summary: Make your repository understandable to humans and AI.
|
|
5
5
|
Author: Willy Cordon
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -82,6 +82,14 @@ klap open
|
|
|
82
82
|
`.klap/`. KlapContext agrega esa carpeta a `.git/info/exclude`; nunca modifica
|
|
83
83
|
tu `.gitignore`.
|
|
84
84
|
|
|
85
|
+
El portal incluye un árbol navegable de archivos y símbolos extraídos del grafo,
|
|
86
|
+
recorridos de llamadas y cambios recientes de Git. El contexto del agente y
|
|
87
|
+
`klap context` usan las mismas rutas y símbolos. El indicador de cobertura cuenta
|
|
88
|
+
archivos de código con al menos un nodo en Graphify: **no mide exhaustividad de
|
|
89
|
+
funciones ni garantiza que se hayan resuelto rutas dinámicas**. Los recorridos
|
|
90
|
+
son caminos del grafo, no afirmaciones de que una ruta HTTP los ejecuta; sus
|
|
91
|
+
ramas pueden requerir inspección del código original.
|
|
92
|
+
|
|
85
93
|
## Comandos
|
|
86
94
|
|
|
87
95
|
| Comando | Descripción |
|
|
@@ -48,6 +48,14 @@ klap open
|
|
|
48
48
|
`.klap/`. KlapContext agrega esa carpeta a `.git/info/exclude`; nunca modifica
|
|
49
49
|
tu `.gitignore`.
|
|
50
50
|
|
|
51
|
+
El portal incluye un árbol navegable de archivos y símbolos extraídos del grafo,
|
|
52
|
+
recorridos de llamadas y cambios recientes de Git. El contexto del agente y
|
|
53
|
+
`klap context` usan las mismas rutas y símbolos. El indicador de cobertura cuenta
|
|
54
|
+
archivos de código con al menos un nodo en Graphify: **no mide exhaustividad de
|
|
55
|
+
funciones ni garantiza que se hayan resuelto rutas dinámicas**. Los recorridos
|
|
56
|
+
son caminos del grafo, no afirmaciones de que una ruta HTTP los ejecuta; sus
|
|
57
|
+
ramas pueden requerir inspección del código original.
|
|
58
|
+
|
|
51
59
|
## Comandos
|
|
52
60
|
|
|
53
61
|
| Comando | Descripción |
|
|
@@ -34,8 +34,11 @@ def compile_context(root: Path, query: str, *, detail: str = "standard", max_tok
|
|
|
34
34
|
context = build(root, {"nodes": []})
|
|
35
35
|
model = context["system_model"]; limit = min(DETAIL_LIMITS[detail], max(1, max_tokens // 350)); terms = _terms(query, plan.area)
|
|
36
36
|
entries = _rank(model["entry_points"], terms, limit); flows = _rank(model["main_flows"], terms, limit); files = _rank(model["important_files"], terms, limit)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
exploration = model.get("exploration", {})
|
|
38
|
+
graph_files = _rank(exploration.get("files", []), terms, limit)
|
|
39
|
+
graph_paths = _rank(exploration.get("call_paths", []), terms, limit)
|
|
40
|
+
read_first = ([{"path": item.get("file"), "reason": "Punto de entrada relacionado", "relevance": item["relevance"]} for item in entries if item.get("file")] + [{"path": item["path"], "reason": "Archivo o símbolo relacionado en el grafo", "relevance": item["relevance"]} for item in graph_files] + files)[:limit]
|
|
41
|
+
result = {"intent": plan.intent, "confidence": plan.confidence, "query": query, "area": plan.area, "detail": detail, "max_tokens": max_tokens, "entry_points": entries, "flows": flows, "graph_call_paths": graph_paths, "symbols": _rank(model["semantic_model"]["components"], terms, limit), "files": files + graph_files, "coverage": exploration.get("coverage", {}), "recent_changes": _rank(model.get("recent_changes", []), terms, min(limit, 3)), "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}
|
|
39
42
|
result["verification"] = reconcile(result["evidence"])
|
|
40
43
|
result = sanitize(result)
|
|
41
44
|
result["estimated_tokens"] = min(max_tokens, len(json.dumps(result, ensure_ascii=False)) // 4)
|
|
@@ -5,9 +5,15 @@ def items(values, key="name"):
|
|
|
5
5
|
return "\n".join(f"- {value.get(key) or value.get('path')}" for value in values) if values else "Sin evidencia disponible."
|
|
6
6
|
def render(context: dict) -> str:
|
|
7
7
|
s=context.get("system_model",{}); purpose=s.get("purpose",{}).get("text") or "No fue posible determinar el propósito con confianza."
|
|
8
|
+
exploration=s.get("exploration",{}); coverage=exploration.get("coverage",{})
|
|
8
9
|
sections=[("Propósito del Proyecto",purpose),("Cómo Funciona el Sistema",s.get("project_story",{}).get("text") or "No fue posible determinarlo con confianza."),("Superficies de Ejecución","\n".join(f"- {x['label']}: {x['count']}" for x in s.get("runtime_surfaces",[])) or "Sin superficies específicas detectadas."),("Rutas",items(s.get("routes",[]))),("Procesos Programados",items(s.get("scheduled_processes",[]))),("Jobs de Cola",items(s.get("queue_jobs",[]))),("Flujos Principales","\n".join(f"- **{x['name']}**: " + " → ".join(step["name"] for step in x["steps"]) for x in s.get("main_flows",[])) or "No se detectaron flujos principales con confianza."),("Dependencias Importantes","\n".join(f"- {x['package']} {x['version']} ({x['scope']})" for x in s.get("dependencies",[])[:12]) or "Sin dependencias específicas detectadas."),("Sistemas Externos",items(s.get("external_systems",[]))),("Almacenes de Datos",items(s.get("datastores",[]))),("Por Dónde Empezar","\n".join(f"- **{x['intent']}**: {', '.join(x['paths'])}" for x in s.get("start_here",[])) or "Sin recomendaciones detectadas."),("Incertidumbres","\n".join(f"- {x}" for x in s.get("unknowns",[])) or "Sin incertidumbres relevantes registradas.")]
|
|
9
10
|
lines=["# KlapContext — Contexto del Proyecto"]
|
|
10
11
|
for title,body in sections: lines.extend(["",f"## {title}",body])
|
|
12
|
+
lines.extend(["", "## Cobertura de la exploración", f"- Archivos de código con nodos: {coverage.get('code_files_with_graph_nodes', 0)}/{coverage.get('eligible_code_files', 0)}; estado: {coverage.get('status', 'UNKNOWN')}.", "- Un archivo representado en el grafo no implica que todas sus funciones estén resueltas.", "", "## Estructura y recorridos de llamadas", "- Consulta `.klap/context.json` → `system_model.exploration.files` para localizar archivos y símbolos por ruta."])
|
|
13
|
+
for path in exploration.get("call_paths", [])[:8]:
|
|
14
|
+
lines.append("- " + " → ".join(f"{step['name']} ({step['file']})" for step in path["steps"]) + " [recorrido del grafo; entrada no confirmada]")
|
|
15
|
+
lines.extend(["", "## Cambios recientes en Git"])
|
|
16
|
+
lines.extend(f"- {change['date']} {change['commit'][:10]}: {change['subject']}" for change in s.get("recent_changes", [])[:6])
|
|
11
17
|
lines.extend(["","## Instrucciones para el Agente","1. Lee este contexto antes de explorar ampliamente el repositorio.","2. Usa Graphify MCP para dependencias y rutas de llamadas.","3. Consulta paths o vecinos relevantes antes de abrir muchos archivos.","4. Lee el código real antes de modificarlo.","5. Trata el contexto inferido como guía, no como verdad absoluta."])
|
|
12
18
|
text="\n".join(lines)+"\n"; return text+f"\nTokens estimados: {len(text.split()) * 4 // 3}\n"
|
|
13
19
|
def write(context: dict, output: Path) -> str:
|
|
@@ -30,14 +30,17 @@ def cmd_init(args):
|
|
|
30
30
|
except (RuntimeError, GraphifyError) as e:
|
|
31
31
|
print(f"✗ {e}", file=sys.stderr); return 1
|
|
32
32
|
print("✓ Git repository detected\n✓ Graphify available\n✓ Graph generated\n✓ Engineering context generated\n✓ Agent context generated\n✓ Human portal generated")
|
|
33
|
+
coverage=context["system_model"]["exploration"]["coverage"]
|
|
34
|
+
print(f"Coverage: {coverage['code_files_with_graph_nodes']}/{coverage['eligible_code_files']} code files represented in graph ({coverage['status']}); graph nodes do not guarantee complete symbol analysis")
|
|
33
35
|
stack_items = [item for values in context["stack"].values() if isinstance(values, list) for item in values]
|
|
34
36
|
print(f"\nProject: {context['project']['name']}\nStack: {' / '.join(stack_items) or 'unknown'}\nContext: CURRENT\n\nHuman portal:\n.klap/index.html\n\nAgent context:\n.klap/agent-context.md")
|
|
35
37
|
return 0
|
|
36
38
|
|
|
37
39
|
def cmd_update(args):
|
|
38
|
-
try: generate_all(root_path(args.path), update=True)
|
|
40
|
+
try: context=generate_all(root_path(args.path), update=True)
|
|
39
41
|
except (RuntimeError, GraphifyError) as e: print(f"✗ {e}", file=sys.stderr); return 1
|
|
40
|
-
|
|
42
|
+
coverage=context["system_model"]["exploration"]["coverage"]
|
|
43
|
+
print(f"✓ KlapContext updated; graph coverage: {coverage['code_files_with_graph_nodes']}/{coverage['eligible_code_files']} code files ({coverage['status']})"); return 0
|
|
41
44
|
|
|
42
45
|
def cmd_status(args):
|
|
43
46
|
root=root_path(args.path); state_file=root/".klap"/"state.json"
|
|
@@ -9,7 +9,8 @@ from .frameworks.generic_php import GenericPhpAdapter
|
|
|
9
9
|
from .frameworks.laravel import LaravelAdapter
|
|
10
10
|
from .frameworks.fastapi import FastAPIAdapter
|
|
11
11
|
from .frameworks.node import NodeAdapter
|
|
12
|
-
from .git import commit
|
|
12
|
+
from .git import commit, recent_changes
|
|
13
|
+
from .exploration import inspect
|
|
13
14
|
from .semantic import SemanticModel
|
|
14
15
|
from .system import deployment, documents, external_systems, purpose
|
|
15
16
|
|
|
@@ -34,7 +35,7 @@ def _edges(graph: dict) -> list[dict]:
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def _modules(nodes: list[dict]) -> list[dict]:
|
|
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]
|
|
38
|
+
return [{"name": item.get("label") or item.get("name") or item.get("id"), "path": item.get("source_file") or item.get("file") or item.get("path"), "kind": item.get("type", "module"), "description": None, "status": "CONFIRMED", "evidence": []} for item in nodes if isinstance(item, dict) and (item.get("label") or item.get("name") or item.get("id")) and (item.get("source_file") or item.get("file") or item.get("path"))][:100]
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def _runtime(points: list[dict], background: list[dict], frameworks: list[str]) -> dict:
|
|
@@ -133,11 +134,16 @@ def build(root: Path, graph: dict) -> dict:
|
|
|
133
134
|
stack, detector_evidence = detect_project(root)
|
|
134
135
|
docs = documents(root); value = purpose(root, docs); systems, stores = external_systems(root); deploy, observability, _ = deployment(root)
|
|
135
136
|
semantic, raw = _semantic(root, stack); model = semantic.as_dict(); model["project_components"] = detect_components(root); points, background = model["entry_points"], model["background_tasks"]
|
|
137
|
+
exploration = inspect(root, graph, model)
|
|
136
138
|
runtime = _runtime(points, background, stack["frameworks"]); inputs, outputs = _io(points, systems, stores); flows = _flows(model)
|
|
137
139
|
important = raw["important_files"] or ([{"path": "README.md", "role": "Documentación", "reason": "README detectado"}] if (root / "README.md").exists() else [])
|
|
138
140
|
tests = [{"path": item, "reason": "Tests detectados"} for item in ("tests", "test", "phpunit.xml", "pytest.ini") if (root / item).exists()]
|
|
139
141
|
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)
|
|
140
142
|
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]}
|
|
143
|
+
system["exploration"] = exploration
|
|
144
|
+
system["recent_changes"] = recent_changes(root, limit=8)
|
|
145
|
+
if exploration["coverage"]["status"] == "PARTIAL":
|
|
146
|
+
system["unknowns"].append("Cobertura parcial del grafo: algunos archivos de código no tienen nodos; consultar el código original.")
|
|
141
147
|
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"]}
|
|
142
148
|
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)}
|
|
143
149
|
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)}
|
|
@@ -26,14 +26,17 @@ def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
|
26
26
|
if value not in stack[bucket]: stack[bucket].append(value)
|
|
27
27
|
evidence.append(Evidence("file", path, reason, "CONFIRMED", 1.0))
|
|
28
28
|
composer = root / "composer.json"
|
|
29
|
+
if not composer.exists():
|
|
30
|
+
composer = next(_sources(root, "composer.json"), composer)
|
|
29
31
|
if composer.exists():
|
|
30
|
-
|
|
32
|
+
composer_path = composer.relative_to(root).as_posix()
|
|
33
|
+
add("languages", "PHP", composer_path, "Composer manifest detected")
|
|
31
34
|
stack["runtime"].append("PHP"); stack["package_managers"].append("composer")
|
|
32
35
|
try: deps = {**json.loads(composer.read_text(encoding="utf-8")).get("require", {}), **json.loads(composer.read_text(encoding="utf-8")).get("require-dev", {})}
|
|
33
36
|
except json.JSONDecodeError: deps = {}
|
|
34
|
-
if "laravel/framework" in deps: add("frameworks", "Laravel",
|
|
37
|
+
if "laravel/framework" in deps: add("frameworks", "Laravel", composer_path, "laravel/framework dependency detected")
|
|
35
38
|
if "laravel/lumen-framework" in deps:
|
|
36
|
-
add("frameworks", "Lumen",
|
|
39
|
+
add("frameworks", "Lumen", composer_path, "laravel/lumen-framework dependency detected")
|
|
37
40
|
stack["project_type"] = "backend-api"
|
|
38
41
|
elif any(_sources(root, "*.php")):
|
|
39
42
|
# A standalone PHP repository has no Composer manifest to identify it,
|
|
@@ -41,23 +44,30 @@ def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
|
41
44
|
first_php = next(_sources(root, "*.php"))
|
|
42
45
|
add("languages", "PHP", str(first_php.relative_to(root)).replace("\\", "/"), "PHP source file detected")
|
|
43
46
|
package = root / "package.json"
|
|
47
|
+
if not package.exists():
|
|
48
|
+
package = next(_sources(root, "package.json"), package)
|
|
44
49
|
if package.exists():
|
|
45
|
-
|
|
50
|
+
package_path = package.relative_to(root).as_posix()
|
|
51
|
+
add("languages", "JavaScript", package_path, "npm manifest detected")
|
|
46
52
|
stack["runtime"].append("Node.js"); stack["package_managers"].append("npm")
|
|
47
53
|
try: deps = {**json.loads(package.read_text(encoding="utf-8")).get("dependencies", {}), **json.loads(package.read_text(encoding="utf-8")).get("devDependencies", {})}
|
|
48
54
|
except json.JSONDecodeError: deps = {}
|
|
49
55
|
for name, label in (("next", "Next.js"), ("vue", "Vue"), ("react", "React"), ("express", "Express"), ("@nestjs/core", "NestJS")):
|
|
50
|
-
if name in deps: add("frameworks", label,
|
|
51
|
-
if any(_sources(root, "*.ts")) or any(_sources(root, "*.tsx")): add("languages", "TypeScript",
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if name in deps: add("frameworks", label, package_path, f"{name} dependency detected")
|
|
57
|
+
if any(_sources(root, "*.ts")) or any(_sources(root, "*.tsx")): add("languages", "TypeScript", package_path, "TypeScript source detected")
|
|
58
|
+
python_manifest = next((path for pattern in ("pyproject.toml", "requirements.txt") if (path := next(_sources(root, pattern), None))), None)
|
|
59
|
+
if python_manifest:
|
|
60
|
+
python_path = python_manifest.relative_to(root).as_posix()
|
|
61
|
+
add("languages", "Python", python_path, "Python manifest detected")
|
|
55
62
|
stack["runtime"].append("Python"); stack["package_managers"].append("pip")
|
|
56
|
-
if "fastapi" in python_manifest.read_text(encoding="utf-8", errors="ignore").casefold(): add("frameworks", "FastAPI",
|
|
63
|
+
if "fastapi" in python_manifest.read_text(encoding="utf-8", errors="ignore").casefold(): add("frameworks", "FastAPI", python_path, "FastAPI dependency detected")
|
|
57
64
|
if (root / "manage.py").exists(): add("frameworks", "Django", "manage.py", "Django entry point detected")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
projects = list(_sources(root, "*.csproj"))
|
|
66
|
+
if projects:
|
|
67
|
+
project = projects[0]; relative = project.relative_to(root).as_posix()
|
|
68
|
+
add("languages", "C#", relative, ".NET project detected"); stack["runtime"].append(".NET"); stack["package_managers"].append("NuGet")
|
|
69
|
+
if "Microsoft.NET.Sdk.Web" in project.read_text(encoding="utf-8", errors="ignore"):
|
|
70
|
+
add("frameworks", "ASP.NET", relative, "Web SDK detected")
|
|
61
71
|
if (root / "go.mod").exists():
|
|
62
72
|
add("languages", "Go", "go.mod", "Go module detected"); stack["runtime"].append("Go"); stack["package_managers"].append("go modules")
|
|
63
73
|
if (root / "Cargo.toml").exists():
|
|
@@ -66,10 +76,10 @@ def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
|
66
76
|
manifest = "pom.xml" if (root / "pom.xml").exists() else "build.gradle" if (root / "build.gradle").exists() else "build.gradle.kts"
|
|
67
77
|
add("languages", "Java", manifest, "Java build manifest detected"); stack["runtime"].append("JVM"); stack["package_managers"].append("maven" if manifest == "pom.xml" else "gradle")
|
|
68
78
|
# 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")):
|
|
79
|
+
for suffix, language in (("*.py", "Python"), ("*.cs", "C#"), ("*.js", "JavaScript"), ("*.ts", "TypeScript"), ("*.java", "Java"), ("*.go", "Go"), ("*.rs", "Rust")):
|
|
70
80
|
if language not in stack["languages"] and any(_sources(root, suffix)):
|
|
71
81
|
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
|
|
82
|
+
if any(name in stack["frameworks"] for name in ("Laravel", "Lumen", "FastAPI", "Express", "NestJS", "ASP.NET")): stack["project_type"] = "backend-api"
|
|
73
83
|
if (root / "Dockerfile").exists(): add("infrastructure", "Docker", "Dockerfile", "Dockerfile detected")
|
|
74
84
|
if list(root.glob("docker-compose*")) or list(root.glob("compose*")): add("infrastructure", "Docker Compose", "docker-compose", "Compose configuration detected")
|
|
75
85
|
if (root / ".github" / "workflows").exists(): add("infrastructure", "GitHub Actions", ".github/workflows", "GitHub Actions workflows detected")
|
|
@@ -79,7 +89,7 @@ def detect_project(root: Path) -> tuple[dict, list[Evidence]]:
|
|
|
79
89
|
def detect_components(root: Path) -> list[dict]:
|
|
80
90
|
"""Find independent project boundaries without assuming a monorepo layout."""
|
|
81
91
|
result = []
|
|
82
|
-
manifests = [path for pattern in ("composer.json", "package.json", "pyproject.toml", "requirements.txt") for path in _sources(root, pattern)]
|
|
92
|
+
manifests = [path for pattern in ("composer.json", "package.json", "pyproject.toml", "requirements.txt", "*.csproj", "go.mod", "Cargo.toml", "pom.xml") for path in _sources(root, pattern)]
|
|
83
93
|
for manifest in manifests:
|
|
84
94
|
directory = manifest.parent
|
|
85
95
|
if directory == root: continue
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Language-neutral, evidence-based navigation of a Graphify code graph."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
EXCLUDED = {".git", ".klap", "graphify-out", "node_modules", "vendor", ".venv", "venv", "build", "dist", "__pycache__"}
|
|
9
|
+
CODE_EXTENSIONS = {".py", ".php", ".cs", ".js", ".jsx", ".ts", ".tsx", ".java", ".go", ".rs", ".rb", ".kt", ".vue", ".swift", ".cpp", ".c", ".h"}
|
|
10
|
+
CALL_RELATIONS = {"calls", "invokes", "dispatches", "calls_method"}
|
|
11
|
+
MEMBER_RELATIONS = {"method", "contains", "defines", "has_method", "member"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _path(root: Path, value: object) -> str | None:
|
|
15
|
+
if not isinstance(value, str) or not value.strip():
|
|
16
|
+
return None
|
|
17
|
+
raw = value.replace("\\", "/")
|
|
18
|
+
candidate = Path(raw)
|
|
19
|
+
try:
|
|
20
|
+
relative = candidate.resolve().relative_to(root.resolve()) if candidate.is_absolute() else candidate
|
|
21
|
+
except (ValueError, OSError):
|
|
22
|
+
return None
|
|
23
|
+
if ".." in relative.parts or any(part in EXCLUDED for part in relative.parts):
|
|
24
|
+
return None
|
|
25
|
+
return relative.as_posix()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _edges(graph: dict) -> list[dict]:
|
|
29
|
+
nested = graph.get("graph", {})
|
|
30
|
+
for container in (graph, nested if isinstance(nested, dict) else {}):
|
|
31
|
+
for key in ("links", "edges"):
|
|
32
|
+
if isinstance(container.get(key), list):
|
|
33
|
+
return container[key]
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def inspect(root: Path, graph: dict, semantic: dict, *, max_files: int = 10000) -> dict:
|
|
38
|
+
"""Keep source paths and graph relationships intact, with explicit limits."""
|
|
39
|
+
nodes = graph.get("nodes", [])
|
|
40
|
+
if not isinstance(nodes, list):
|
|
41
|
+
nodes = []
|
|
42
|
+
by_id = {str(n["id"]): n for n in nodes if isinstance(n, dict) and n.get("id") is not None}
|
|
43
|
+
files: dict[str, dict] = {}
|
|
44
|
+
for directory, children, names in os.walk(root):
|
|
45
|
+
children[:] = sorted(c for c in children if c not in EXCLUDED)
|
|
46
|
+
for name in sorted(names):
|
|
47
|
+
path = _path(root, str(Path(directory) / name))
|
|
48
|
+
if path:
|
|
49
|
+
files[path] = {"path": path, "symbols": [], "analyzed": False}
|
|
50
|
+
if len(files) >= max_files:
|
|
51
|
+
break
|
|
52
|
+
if len(files) >= max_files:
|
|
53
|
+
break
|
|
54
|
+
eligible = {path for path in files if Path(path).suffix.lower() in CODE_EXTENSIONS}
|
|
55
|
+
analyzed: set[str] = set()
|
|
56
|
+
for node in nodes:
|
|
57
|
+
if not isinstance(node, dict):
|
|
58
|
+
continue
|
|
59
|
+
path = _path(root, node.get("source_file") or node.get("file") or node.get("path"))
|
|
60
|
+
if not path or path not in files:
|
|
61
|
+
continue
|
|
62
|
+
analyzed.add(path)
|
|
63
|
+
symbol = {"id": str(node.get("id", "")), "name": str(node.get("label") or node.get("name") or node.get("id")),
|
|
64
|
+
"kind": str(node.get("type") or "symbol"), "line": node.get("source_location") or node.get("line"), "source": "graphify"}
|
|
65
|
+
files[path]["symbols"].append(symbol)
|
|
66
|
+
for component in semantic.get("components", []):
|
|
67
|
+
path = _path(root, component.get("file"))
|
|
68
|
+
if path in files and not files[path]["symbols"]:
|
|
69
|
+
files[path]["symbols"].append({"id": component.get("symbol") or component["name"], "name": component["name"], "kind": component.get("type", "symbol"), "line": None, "source": "klap"})
|
|
70
|
+
for path, item in files.items():
|
|
71
|
+
item["analyzed"] = path in analyzed
|
|
72
|
+
|
|
73
|
+
graph_edges = _edges(graph)
|
|
74
|
+
members: dict[str, list[dict]] = defaultdict(list)
|
|
75
|
+
calls: dict[str, list[dict]] = defaultdict(list)
|
|
76
|
+
incoming: set[str] = set()
|
|
77
|
+
for edge in graph_edges:
|
|
78
|
+
if not isinstance(edge, dict):
|
|
79
|
+
continue
|
|
80
|
+
source, target = str(edge.get("source", edge.get("from", ""))), str(edge.get("target", edge.get("to", "")))
|
|
81
|
+
if source not in by_id or target not in by_id:
|
|
82
|
+
continue
|
|
83
|
+
relation = str(edge.get("relation") or edge.get("type") or "").lower()
|
|
84
|
+
if relation in MEMBER_RELATIONS:
|
|
85
|
+
members[source].append({"id": target, "name": str(by_id[target].get("label") or target), "relation": relation})
|
|
86
|
+
if relation in CALL_RELATIONS:
|
|
87
|
+
calls[source].append({"id": target, "relation": relation, "confidence": edge.get("confidence", "UNKNOWN")})
|
|
88
|
+
incoming.add(target)
|
|
89
|
+
for file in files.values():
|
|
90
|
+
for symbol in file["symbols"]:
|
|
91
|
+
symbol["members"] = members.get(symbol["id"], [])[:100]
|
|
92
|
+
|
|
93
|
+
# These are graph-backed call paths, not claims that an HTTP request invokes them.
|
|
94
|
+
paths = []
|
|
95
|
+
seeds = [key for key in calls if key not in incoming and _path(root, by_id[key].get("source_file")) in eligible]
|
|
96
|
+
for key in sorted(seeds, key=lambda k: str(by_id[k].get("label", k))):
|
|
97
|
+
chain, visited, current = [], set(), key
|
|
98
|
+
while current in by_id and current not in visited and len(chain) < 6:
|
|
99
|
+
visited.add(current)
|
|
100
|
+
node = by_id[current]
|
|
101
|
+
chain.append({"name": str(node.get("label") or current), "file": _path(root, node.get("source_file")), "id": current})
|
|
102
|
+
neighbors = calls.get(current, [])
|
|
103
|
+
if not neighbors:
|
|
104
|
+
break
|
|
105
|
+
current = neighbors[0]["id"]
|
|
106
|
+
if len(chain) > 1:
|
|
107
|
+
paths.append({"name": chain[0]["name"], "steps": chain, "status": "INFERRED", "source": "graphify",
|
|
108
|
+
"note": "Recorrido de llamadas del grafo; no confirma un punto de entrada ni el orden de otras ramas.",
|
|
109
|
+
"branches": sum(max(0, len(calls.get(step["id"], [])) - 1) for step in chain)})
|
|
110
|
+
if len(paths) >= 40:
|
|
111
|
+
break
|
|
112
|
+
return {"files": list(files.values()), "call_paths": paths,
|
|
113
|
+
"coverage": {"files_listed": len(files), "eligible_code_files": len(eligible),
|
|
114
|
+
"code_files_with_graph_nodes": len(eligible & analyzed),
|
|
115
|
+
"graph_nodes": len(nodes), "graph_edges": len(graph_edges),
|
|
116
|
+
"truncated": len(files) >= max_files,
|
|
117
|
+
"status": "PARTIAL" if eligible - analyzed or len(files) >= max_files else "REPRESENTED",
|
|
118
|
+
"note": "Un nodo en un archivo indica presencia en el grafo, no análisis exhaustivo de todas sus funciones."}}
|
|
@@ -19,7 +19,10 @@ def _read(path: Path) -> str:
|
|
|
19
19
|
|
|
20
20
|
def _symbol(target: str) -> str | None:
|
|
21
21
|
match = re.search(r"(?:\[\s*)?([A-Za-z_]\w*(?:\\[A-Za-z_]\w*)*)::class\s*,\s*['\"](\w+)['\"]", target)
|
|
22
|
-
|
|
22
|
+
if not match:
|
|
23
|
+
return None
|
|
24
|
+
class_name = match.group(1).rsplit("\\", 1)[-1]
|
|
25
|
+
return f"{class_name}::{match.group(2)}"
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
def _e(root: Path, path: Path, offset: int, reason: str, *, symbol: str | None = None, status: str = "CONFIRMED") -> list[dict]:
|
|
@@ -43,7 +43,7 @@ def system_map(interactions: dict) -> str:
|
|
|
43
43
|
if edge.get("source") in points and edge.get("target") in points
|
|
44
44
|
)
|
|
45
45
|
boxes = "".join(
|
|
46
|
-
f"<g class='map-node'><rect x='{points[node['name']][0] - 65}' y='{points[node['name']][1] - 24}' width='130' height='48' rx='8'/>"
|
|
46
|
+
f"<g class='map-node' tabindex='0' role='button' data-node='{e(node['name'])}' data-kind='{e(node.get('type', 'componente'))}'><rect x='{points[node['name']][0] - 65}' y='{points[node['name']][1] - 24}' width='130' height='48' rx='8'/>"
|
|
47
47
|
f"<text x='{points[node['name']][0]}' y='{points[node['name']][1] - 2}'>{e(node['name'])[:18]}</text>"
|
|
48
48
|
f"<text class='map-kind' x='{points[node['name']][0]}' y='{points[node['name']][1] + 13}'>{e(node.get('type', 'componente'))[:18]}</text></g>"
|
|
49
49
|
for node in nodes
|
|
@@ -60,6 +60,29 @@ def section(title: str, content: str, view: str, icon: str) -> str:
|
|
|
60
60
|
return f"<section class='view' data-view='{view}' aria-label='{e(title)}'><div class='view-title'><span>{icon}</span><div><h2>{e(title)}</h2><p>Información extraída del repositorio y respaldada por evidencia.</p></div></div>{content}</section>"
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def file_tree(files: list[dict]) -> str:
|
|
64
|
+
tree: dict = {}
|
|
65
|
+
for item in files:
|
|
66
|
+
cursor = tree
|
|
67
|
+
parts = item["path"].split("/")
|
|
68
|
+
for part in parts[:-1]:
|
|
69
|
+
cursor = cursor.setdefault(part, {})
|
|
70
|
+
cursor[parts[-1]] = item
|
|
71
|
+
|
|
72
|
+
def branch(directory: dict) -> str:
|
|
73
|
+
rows = []
|
|
74
|
+
for name, value in sorted(directory.items(), key=lambda pair: ("path" in pair[1], pair[0].lower())):
|
|
75
|
+
if "path" not in value:
|
|
76
|
+
rows.append(f"<details class='folder'><summary>▸ {e(name)}</summary>{branch(value)}</details>")
|
|
77
|
+
continue
|
|
78
|
+
symbols = value.get("symbols", [])
|
|
79
|
+
details = "".join(f"<li><b>{e(symbol['name'])}</b> <small>{e(symbol['kind'])} · {e(symbol['source'])}</small>" +
|
|
80
|
+
("<ul>" + "".join(f"<li>{e(member['name'])}</li>" for member in symbol.get("members", [])) + "</ul>" if symbol.get("members") else "") + "</li>" for symbol in symbols[:100])
|
|
81
|
+
rows.append(f"<details class='source-file'><summary>{e(name)} <small>{len(symbols)} símbolos · {'grafo' if value['analyzed'] else 'sin nodos del grafo'}</small></summary><p>{e(value['path'])}</p><ul>{details or '<li>Sin símbolos detectados.</li>'}</ul></details>")
|
|
82
|
+
return "".join(rows)
|
|
83
|
+
return branch(tree) or empty("No se encontraron archivos.")
|
|
84
|
+
|
|
85
|
+
|
|
63
86
|
def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
|
|
64
87
|
project = context["project"]
|
|
65
88
|
system = context.get("system_model", {})
|
|
@@ -72,6 +95,11 @@ def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
|
|
|
72
95
|
framework = system.get("framework") or {}
|
|
73
96
|
purpose = system.get("purpose", {}).get("text") or "No pudimos determinar el propósito con suficiente confianza."
|
|
74
97
|
story = system.get("project_story", {}).get("text") or system.get("runtime", {}).get("description") or "Todavía no hay una narrativa técnica verificable."
|
|
98
|
+
exploration = system.get("exploration", {})
|
|
99
|
+
coverage = exploration.get("coverage", {})
|
|
100
|
+
graph_paths = exploration.get("call_paths", [])
|
|
101
|
+
call_cards = "".join(f"<article class='flow-card'><b>{e(path['name'])}</b><p>{' <em>→</em> '.join(e(step['name']) for step in path['steps'])}</p><small>{e(path['note'])} Ramas adicionales: {path['branches']}.</small></article>" for path in graph_paths)
|
|
102
|
+
recent = "".join(f"<li><strong>{e(change['subject'])}</strong><small>{e(change['date'])} · {e(change['commit'][:10])}</small></li>" for change in system.get("recent_changes", []))
|
|
75
103
|
state = "ACTUAL" if project.get("git_commit") else "SIN COMMIT"
|
|
76
104
|
tags = "".join(f"<span>{e(item)}</span>" for item in stack[:6])
|
|
77
105
|
if framework.get("version"):
|
|
@@ -113,10 +141,10 @@ def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
|
|
|
113
141
|
</section>
|
|
114
142
|
<section class='map-card' aria-label='Mapa de interacción del sistema'><header><div><span class='icon'>⌘</span><div><h2>Mapa del sistema</h2><p>Componentes, relaciones y puntos de entrada en una sola lectura.</p></div></div><button class='outline' data-go='tecnico'>Explorar evidencia <span>→</span></button></header>{system_map(system.get('system_interactions', {}))}</section>
|
|
115
143
|
<section class='quick-grid'>
|
|
116
|
-
<article><b>{len(points)}</b><span>Puntos de entrada</span><small>CLI, HTTP, archivos o scheduler</small></article>
|
|
117
|
-
<article><b>{len(flows)}</b><span>Flujos principales</span><small>Procesos y secuencias detectadas</small></article>
|
|
118
|
-
<article><b>{len(evidence)}</b><span>Hechos confirmados</span><small>Con respaldo en el código</small></article>
|
|
119
|
-
<article class='attention'><b>{len(unknowns)}</b><span>
|
|
144
|
+
<article data-go='entradas'><b>{len(points)}</b><span>Puntos de entrada</span><small>CLI, HTTP, archivos o scheduler</small></article>
|
|
145
|
+
<article data-go='flujos'><b>{len(flows)}</b><span>Flujos principales</span><small>Procesos y secuencias detectadas</small></article>
|
|
146
|
+
<article data-go='evidencia'><b>{len(evidence)}</b><span>Hechos confirmados</span><small>Con respaldo en el código</small></article>
|
|
147
|
+
<article class='attention' data-go='evidencia'><b>{len(unknowns)}</b><span>Pendientes de validar</span><small>Aspectos con evidencia insuficiente</small></article>
|
|
120
148
|
</section>
|
|
121
149
|
<section class='dashboard-bottom'><div class='compact-panel'><header><h2>Cómo se ejecuta</h2><button class='text-button' data-go='entradas'>Ver entradas →</button></header><div class='surfaces'>{surface_cards}</div></div>
|
|
122
150
|
<div class='compact-panel'><header><h2>Lectura recomendada</h2><button class='text-button' data-go='flujos'>Ver flujos →</button></header><ul class='start-list'>{start_rows}</ul></div></section>
|
|
@@ -127,12 +155,16 @@ def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
|
|
|
127
155
|
section("Entradas", f"<div class='detail-grid'><article class='detail-card'><h3>Puntos de entrada</h3><ul class='detail-list'>{item_rows(points, 'intent')}</ul></article><article class='detail-card'><h3>Entradas y salidas</h3><div class='columns'><div><label>ENTRADAS</label><ul class='detail-list'>{item_rows(system.get('inputs', []), 'description')}</ul></div><div><label>SALIDAS</label><ul class='detail-list'>{item_rows(system.get('outputs', []), 'description')}</ul></div></div></article></div>", "entradas", "⇢"),
|
|
128
156
|
section("Evidencia", f"<article class='detail-card evidence-card'><h3>Hechos que sostienen el mapa</h3><ul class='detail-list evidence-list'>{evidence_rows}</ul></article><article class='detail-card unknown-card'><h3>Lo que todavía no sabemos</h3><ul class='detail-list'>{item_rows([{'name': item, 'type': 'requiere validación'} for item in unknowns])}</ul></article>", "evidencia", "◈"),
|
|
129
157
|
section("Exploración técnica", f"<div class='tech-hero'><h3>Profundizá en el análisis del código</h3><p>{e(story)}</p></div><div class='graph-links'>{graph_links}</div><div class='detail-grid'><article class='detail-card'><h3>Dependencias</h3><ul class='detail-list'>{item_rows(system.get('dependencies', []), 'package')}</ul></article><article class='detail-card'><h3>Procesos en segundo plano</h3><ul class='detail-list'>{item_rows(system.get('background_processes', []))}</ul></article></div>", "tecnico", "⌬"),
|
|
158
|
+
section("Archivos y símbolos", f"<div class='tech-hero'><h3>Explorá el proyecto</h3><p>{coverage.get('code_files_with_graph_nodes', 0)} de {coverage.get('eligible_code_files', 0)} archivos de código tienen nodos en el grafo. Estado: {e(coverage.get('status', 'UNKNOWN'))}. Un nodo no garantiza cobertura completa.</p></div><input id='file-search' type='search' placeholder='Buscar archivo o símbolo' aria-label='Buscar archivo o símbolo'><div class='file-tree'>{file_tree(exploration.get('files', []))}</div>", "archivos", "▤"),
|
|
159
|
+
section("Recorridos del grafo", f"<p class='empty'>Estas rutas muestran llamadas conectadas en Graphify. No implican que sean entradas HTTP ni describen todas las ramas.</p><div class='flow-grid'>{call_cards or empty('No hay recorridos de llamadas verificables en el grafo.')}</div>", "recorridos", "⌁"),
|
|
160
|
+
section("Historial Git", f"<article class='detail-card'><h3>Cambios recientes</h3><p>Los mensajes describen cambios registrados; el código actual sigue siendo la fuente para comprender el comportamiento.</p><ul class='detail-list'>{recent or empty('No hay historial disponible.')}</ul></article>", "historial", "◷"),
|
|
130
161
|
]
|
|
131
162
|
agent = json.dumps(agent_text).replace("</", "<\\/")
|
|
132
163
|
return f"""<!doctype html><html lang='es'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>KlapContext · {e(project['name'])}</title>
|
|
133
164
|
<style>
|
|
134
165
|
:root{{--bg:#07111f;--panel:#0c1a2d;--panel-2:#0e2036;--line:#1b3650;--ink:#edf7ff;--muted:#92a7be;--cyan:#16e0c4;--blue:#3d9cff;--violet:#976dff;--amber:#ffb648}}*{{box-sizing:border-box}}body{{margin:0;min-height:100vh;color:var(--ink);font:14px/1.45 Inter,ui-sans-serif,system-ui,sans-serif;background:radial-gradient(900px 520px at 52% -130px,#153d5c70,transparent 70%),var(--bg)}}button,select{{font:inherit}}.top{{height:56px;display:flex;align-items:center;gap:14px;padding:0 18px;border-bottom:1px solid var(--line);background:#081422e8;backdrop-filter:blur(16px)}}.brand{{display:flex;align-items:center;gap:9px;font-weight:800;font-size:13px}}.logo{{display:grid;place-items:center;width:28px;height:28px;border-radius:7px;color:#051525;background:linear-gradient(135deg,var(--cyan),var(--violet));box-shadow:0 0 18px #16e0c455}}.top-title{{display:flex;flex-direction:column;border-left:1px solid var(--line);padding-left:14px}}.top-title b{{font-size:12px}}.top-title small{{color:var(--muted);font-size:10px}}.top-state{{margin-left:auto;color:var(--cyan);font:10px ui-monospace,monospace}}.repo{{padding:6px 9px;border:1px solid var(--line);border-radius:6px;color:#c5d8ed;font:10px ui-monospace,monospace}}.shell{{display:grid;grid-template-columns:174px minmax(0,1fr) 260px;min-height:calc(100vh - 56px)}}.side{{padding:14px 10px;border-right:1px solid var(--line);background:#081422aa}}.side button{{display:flex;width:100%;align-items:center;gap:9px;padding:10px;border:0;border-radius:6px;background:transparent;color:var(--muted);text-align:left;cursor:pointer;font-size:12px}}.side button:hover,.side button.active{{background:linear-gradient(90deg,#0c514d88,#0c243b);color:var(--ink);box-shadow:inset 2px 0 var(--cyan)}}.side .nav-icon{{color:var(--cyan);font-size:16px;width:19px;text-align:center}}.side-footer{{margin-top:28px;padding-top:16px;border-top:1px solid var(--line)}}main{{padding:16px;min-width:0;max-width:1220px;width:100%;margin:auto}}.view{{display:none;animation:in .28s ease}}.view.active{{display:block}}@keyframes in{{from{{opacity:.35;transform:translateY(6px)}}to{{opacity:1;transform:none}}}}.view-title{{display:none}}.overview-head{{display:flex;justify-content:space-between;gap:24px;padding:5px 4px 15px}}.eyebrow,label{{color:var(--cyan);font:10px ui-monospace,monospace;letter-spacing:.13em}}h1{{margin:3px 0 4px;font-size:25px;letter-spacing:-.045em}}h2,h3,p{{margin-top:0}}.overview-head p{{max-width:760px;margin-bottom:9px;color:#c6d7e7;font-size:12px}}.overview-state{{display:flex;align-items:flex-end;flex-direction:column;gap:5px;white-space:nowrap}}.live{{color:var(--cyan);font:10px ui-monospace,monospace}}.overview-state small{{color:var(--muted);font-size:10px}}.tags{{display:flex;gap:5px;flex-wrap:wrap}}.tags span{{padding:3px 6px;border:1px solid #29506b;border-radius:4px;color:#aac0d8;font:9px ui-monospace,monospace}}.map-card,.compact-panel,.detail-card,.flow-card,.trace-box{{border:1px solid var(--line);border-radius:9px;background:linear-gradient(135deg,#0d2037e8,#091727e8);box-shadow:0 18px 35px #00000018}}header{{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:13px 14px;border-bottom:1px solid #1a334c}}header>div{{display:flex;align-items:center;gap:9px}}header h2{{margin:0;font-size:13px}}header p{{margin:2px 0 0;color:var(--muted);font-size:10px}}.icon{{display:grid;place-items:center;width:28px;height:28px;border-radius:6px;color:var(--cyan);background:#0a3b4277;font-size:17px}}.outline,.text-button{{border:1px solid #24658a;border-radius:6px;background:#0a1a2d;color:var(--cyan);padding:7px 9px;font-size:10px;font-weight:700;cursor:pointer}}.text-button{{border:0;background:transparent;padding:0}}.map-shell{{padding:9px 12px 10px;background:radial-gradient(circle at 50% 42%,#123e5570,transparent 60%)}}.map-legend{{display:flex;justify-content:space-between;color:var(--muted);font:9px ui-monospace,monospace}}.map-legend i{{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--cyan);margin-right:4px}}.system-map{{display:block;width:100%;height:min(33vh,280px)}}.map-links path{{fill:none;stroke:#22e2cf9c;stroke-width:1.5;filter:drop-shadow(0 0 3px #20e3d055)}}.map-node rect{{fill:#102b49;stroke:#2f83b4;stroke-width:1}}.map-node text{{fill:#e7f5ff;font:10px ui-monospace,monospace;text-anchor:middle}}.map-node .map-kind{{fill:#84a3bd;font-size:8px}}.map-empty{{padding:58px;text-align:center;color:var(--muted)}}.quick-grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin:10px 0}}.quick-grid article{{min-height:85px;padding:11px;border:1px solid var(--line);border-radius:7px;background:#0b192b}}.quick-grid b{{display:block;color:#f2f8ff;font-size:24px;letter-spacing:-.06em}}.quick-grid span{{display:block;font-size:10px;font-weight:700}}.quick-grid small{{display:block;margin-top:4px;color:var(--muted);font-size:9px}}.quick-grid .attention b{{color:var(--amber)}}.dashboard-bottom,.detail-grid{{display:grid;grid-template-columns:1fr 1fr;gap:10px}}.compact-panel header{{padding:11px 13px}}.compact-panel h2{{font-size:12px}}.surfaces{{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;padding:10px}}.surface{{padding:8px;border-radius:5px;background:#091628;border:1px solid #1c3a54}}.surface b{{display:block;color:var(--cyan);font-size:18px}}.surface span{{color:var(--muted);font-size:9px}}.start-list,.detail-list{{list-style:none;margin:0;padding:8px 13px}}.start-list li{{padding:5px 0;border-bottom:1px solid #1a324a}}.start-list li:last-child,.detail-list li:last-child{{border:0}}.start-list b,.detail-list strong{{display:block;font-size:11px}}.start-list small,.detail-list small{{display:block;color:var(--muted);font-size:9px}}.empty{{padding:8px 13px;color:var(--muted);font-size:11px}}.right{{padding:16px 11px;border-left:1px solid var(--line);background:#081422aa}}.agent-card,.insight-card{{padding:14px;margin-bottom:10px;border:1px solid var(--line);border-radius:8px;background:linear-gradient(145deg,#122a47,#0b182b)}}.agent-card{{border-color:#317b8d}}.agent-card h3,.insight-card h3{{margin-bottom:7px;font-size:12px}}.agent-card p,.insight-card p{{color:#b4c5d7;font-size:10px}}.copy{{width:100%;border:0;border-radius:5px;padding:9px;background:linear-gradient(90deg,var(--cyan),#46d1e7);color:#03202a;font-weight:800;cursor:pointer;font-size:11px}}.insight-card ul{{list-style:none;margin:0;padding:0}}.insight-card li{{padding:7px 0;border-bottom:1px solid #22405a;color:#c0d2e4;font-size:10px}}.insight-card li:last-child{{border:0}}.view-title{{margin:0 0 13px;align-items:center;gap:9px}}.view-title>span{{display:grid;place-items:center;width:31px;height:31px;border-radius:7px;background:#0a3b4277;color:var(--cyan);font-size:19px}}.view-title h2{{margin:0;font-size:18px}}.view-title p{{margin:2px 0 0;color:var(--muted);font-size:10px}}.flow-grid{{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}}.flow-card{{padding:15px}}.flow-card b{{font-size:13px}}.flow-card p{{margin:8px 0 0;color:#b9cce0;font:10px/1.8 ui-monospace,monospace}}em{{color:var(--cyan);font-style:normal}}.trace-box{{margin-top:10px;padding:14px}}select{{display:block;margin:7px 0;padding:8px;border:1px solid var(--line);border-radius:5px;background:#081727;color:var(--ink);font-size:11px}}#flow-trace{{color:var(--cyan);font:11px/1.7 ui-monospace,monospace}}.detail-card{{padding:15px}}.detail-card h3{{font-size:13px}}.detail-list{{padding:0}}.detail-list li{{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px solid #1a324a}}.dot{{display:block;flex:none;width:6px;height:6px;margin-top:5px;border-radius:50%;background:var(--violet);box-shadow:0 0 7px var(--violet)}}.evidence-list mark{{margin-left:auto;border-radius:3px;padding:2px 4px;background:#10493f;color:var(--cyan);font:8px ui-monospace,monospace}}.unknown-card{{margin-top:10px}}.columns{{display:grid;grid-template-columns:1fr 1fr;gap:18px}}.tech-hero{{padding:19px;border:1px solid #2f4968;border-radius:9px;background:linear-gradient(120deg,#172d50,#10192f)}}.tech-hero h3{{margin-bottom:6px;font-size:16px}}.tech-hero p{{margin:0;color:#b4c7dc;font-size:12px}}.graph-links{{display:flex;gap:10px;margin:10px 0}}.graph-links a{{flex:1;padding:13px;border:1px solid #2c5875;border-radius:7px;background:#0b1a2d;color:#d9efff;text-decoration:none;font-size:11px;font-weight:700}}.graph-links a span{{float:right;color:var(--cyan)}}@media(max-width:1050px){{.shell{{grid-template-columns:160px minmax(0,1fr)}}.right{{display:none}}}}@media(max-width:720px){{.shell{{display:block}}.side{{display:flex;overflow:auto;padding:8px;border-bottom:1px solid var(--line)}}.side button{{min-width:max-content}}.side-footer{{display:none}}.top-title{{display:none}}.repo{{display:none}}main{{padding:10px}}.overview-head{{display:block}}.overview-state{{margin-top:10px;align-items:flex-start}}.quick-grid{{grid-template-columns:repeat(2,1fr)}}.dashboard-bottom,.detail-grid,.flow-grid,.columns{{grid-template-columns:1fr}}.system-map{{width:680px;max-width:none}}.map-shell{{overflow:auto}}.surfaces{{grid-template-columns:repeat(2,1fr)}}}}
|
|
135
|
-
|
|
166
|
+
.file-tree{{max-height:70vh;overflow:auto;margin-top:12px;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--panel)}}.file-tree details{{padding:3px 4px 3px 14px;border-left:1px solid var(--line)}}.file-tree summary{{cursor:pointer;padding:5px;color:var(--ink)}}.file-tree small{{color:var(--muted)}}.source-file ul{{padding-left:22px}}.source-file li{{padding:3px}}#file-search{{width:100%;padding:11px;margin-top:12px;background:var(--panel);border:1px solid var(--line);border-radius:7px;color:var(--ink)}}
|
|
167
|
+
</style></head><body><nav class='top'><div class='brand'><span class='logo'>K</span>klap_context</div><div class='top-title'><b>Comprender el sistema</b><small>Explorá componentes, flujos y evidencia de tu repositorio.</small></div><span class='top-state'>● {state}</span><span class='repo'>{e(project['name'])}</span></nav><div class='shell'><aside class='side'><button class='active' data-go='sistema'><span class='nav-icon'>⌘</span>Sistema</button><button data-go='flujos'><span class='nav-icon'>⌁</span>Flujos</button><button data-go='entradas'><span class='nav-icon'>⇢</span>Entradas</button><button data-go='evidencia'><span class='nav-icon'>◈</span>Evidencias</button><button data-go='archivos'><span class='nav-icon'>▤</span>Archivos</button><button data-go='recorridos'><span class='nav-icon'>⌁</span>Recorridos</button><button data-go='historial'><span class='nav-icon'>◷</span>Git</button><button data-go='tecnico'><span class='nav-icon'>⌬</span>Graphify</button><div class='side-footer'><button data-go='evidencia'><span class='nav-icon'>?</span>Ayuda y confianza</button></div></aside><main>{''.join(views)}</main><aside class='right'><section class='agent-card'><h3>✦ Acciones para agente</h3><p>Generá un briefing listo para usar y profundizá luego en el sistema.</p><button class='copy' onclick='copyContext(this)'>▣ Copiar contexto</button></section><section class='insight-card'><h3>💡 Hallazgos principales <span>{min(len(evidence), 9)}</span></h3><ul><li>{len(points)} punto(s) de entrada detectado(s).</li><li>{len(flows)} flujo(s) principal(es) disponible(s).</li><li>{len(evidence)} hecho(s) con respaldo en el código.</li></ul></section><section class='insight-card'><h3>⚡ Siguiente paso</h3><p>Usá las vistas laterales para investigar un área, sin perder el panorama inicial.</p></section></aside></div><script>const agentContext={agent};const views=[...document.querySelectorAll('.view')];const nav=[...document.querySelectorAll('[data-go]')];function go(name){{views.forEach(view=>view.classList.toggle('active',view.dataset.view===name));nav.forEach(button=>button.classList.toggle('active',button.dataset.go===name));history.replaceState(null,'','#'+name);window.scrollTo({{top:0,behavior:'smooth'}})}}nav.forEach(button=>button.addEventListener('click',()=>go(button.dataset.go)));go(location.hash.slice(1)||'sistema');function copyContext(button){{navigator.clipboard.writeText(agentContext).then(()=>{{const old=button.textContent;button.textContent='✓ Contexto copiado';setTimeout(()=>button.textContent=old,1500)}})}}const paths={traces};const search=document.querySelector('#file-search');if(search)search.addEventListener('input',()=>{{const term=search.value.toLocaleLowerCase();document.querySelectorAll('.source-file').forEach(file=>{{file.hidden=!file.textContent.toLocaleLowerCase().includes(term)}});document.querySelectorAll('.folder').forEach(folder=>{{folder.open=!!term}})}});const selector=document.querySelector('#flow-select');if(selector){{const trace=document.querySelector('#flow-trace');const show=()=>trace.textContent=paths[selector.value].join(' → ');selector.addEventListener('change',show);show();}}</script></body></html>"""
|
|
136
168
|
|
|
137
169
|
|
|
138
170
|
def write(context: dict, agent_text: str, graphify_files: list[str], output: Path) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: klapcontext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Make your repository understandable to humans and AI.
|
|
5
5
|
Author: Willy Cordon
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -82,6 +82,14 @@ klap open
|
|
|
82
82
|
`.klap/`. KlapContext agrega esa carpeta a `.git/info/exclude`; nunca modifica
|
|
83
83
|
tu `.gitignore`.
|
|
84
84
|
|
|
85
|
+
El portal incluye un árbol navegable de archivos y símbolos extraídos del grafo,
|
|
86
|
+
recorridos de llamadas y cambios recientes de Git. El contexto del agente y
|
|
87
|
+
`klap context` usan las mismas rutas y símbolos. El indicador de cobertura cuenta
|
|
88
|
+
archivos de código con al menos un nodo en Graphify: **no mide exhaustividad de
|
|
89
|
+
funciones ni garantiza que se hayan resuelto rutas dinámicas**. Los recorridos
|
|
90
|
+
son caminos del grafo, no afirmaciones de que una ruta HTTP los ejecuta; sus
|
|
91
|
+
ramas pueden requerir inspección del código original.
|
|
92
|
+
|
|
85
93
|
## Comandos
|
|
86
94
|
|
|
87
95
|
| Comando | Descripción |
|
|
@@ -7,6 +7,7 @@ src/klapcontext/cli.py
|
|
|
7
7
|
src/klapcontext/context_builder.py
|
|
8
8
|
src/klapcontext/detector.py
|
|
9
9
|
src/klapcontext/evidence.py
|
|
10
|
+
src/klapcontext/exploration.py
|
|
10
11
|
src/klapcontext/git.py
|
|
11
12
|
src/klapcontext/graphify.py
|
|
12
13
|
src/klapcontext/portal.py
|
|
@@ -39,4 +40,5 @@ src/klapcontext/providers/code_intelligence.py
|
|
|
39
40
|
src/klapcontext/providers/graphify.py
|
|
40
41
|
src/klapcontext/providers/javascript_intelligence.py
|
|
41
42
|
src/klapcontext/providers/python_intelligence.py
|
|
43
|
+
tests/test_exploration.py
|
|
42
44
|
tests/test_klapcontext.py
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""End-to-end checks for the graph-to-context navigation boundary."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
10
|
+
|
|
11
|
+
from klapcontext.agent.compiler import compile_context
|
|
12
|
+
from klapcontext.agent_context import render as agent_render
|
|
13
|
+
from klapcontext.context_builder import build
|
|
14
|
+
from klapcontext.detector import detect_project
|
|
15
|
+
from klapcontext.portal import render as portal_render
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GraphExplorationTests(unittest.TestCase):
|
|
19
|
+
def test_csharp_graph_reaches_agent_and_portal(self):
|
|
20
|
+
with TemporaryDirectory() as directory:
|
|
21
|
+
root = Path(directory)
|
|
22
|
+
controller = root / "src" / "InfractionsController.cs"
|
|
23
|
+
controller.parent.mkdir()
|
|
24
|
+
controller.write_text("class InfractionsController {}")
|
|
25
|
+
graph = {"nodes": [
|
|
26
|
+
{"id": "controller", "label": "InfractionsController", "source_file": "src/InfractionsController.cs", "type": "class"},
|
|
27
|
+
{"id": "list", "label": "List", "source_file": "src/InfractionsController.cs", "type": "method"},
|
|
28
|
+
{"id": "service", "label": "FindInfractions", "source_file": "src/InfractionsController.cs", "type": "method"},
|
|
29
|
+
], "links": [{"source": "controller", "target": "list", "relation": "method"},
|
|
30
|
+
{"source": "list", "target": "service", "relation": "calls", "confidence": "EXTRACTED"}]}
|
|
31
|
+
context = build(root, graph)
|
|
32
|
+
files = context["system_model"]["exploration"]["files"]
|
|
33
|
+
self.assertEqual(files[0]["symbols"][0]["members"][0]["name"], "List")
|
|
34
|
+
self.assertEqual(context["system_model"]["exploration"]["call_paths"][0]["steps"][1]["name"], "FindInfractions")
|
|
35
|
+
page = portal_render(context, agent_render(context), [])
|
|
36
|
+
self.assertIn("InfractionsController.cs", page)
|
|
37
|
+
self.assertIn("FindInfractions", page)
|
|
38
|
+
self.assertIn("Archivos y símbolos", page)
|
|
39
|
+
cache = root / ".klap"
|
|
40
|
+
cache.mkdir()
|
|
41
|
+
import json
|
|
42
|
+
(cache / "context.json").write_text(json.dumps(context))
|
|
43
|
+
task = compile_context(root, "InfractionsController", max_tokens=1000)
|
|
44
|
+
self.assertEqual(task["read_first"][0]["path"], "src/InfractionsController.cs")
|
|
45
|
+
|
|
46
|
+
def test_partial_coverage_does_not_claim_full_scan(self):
|
|
47
|
+
with TemporaryDirectory() as directory:
|
|
48
|
+
root = Path(directory)
|
|
49
|
+
(root / "app.py").write_text("def run(): pass")
|
|
50
|
+
context = build(root, {"nodes": [], "links": []})
|
|
51
|
+
coverage = context["system_model"]["exploration"]["coverage"]
|
|
52
|
+
self.assertEqual(coverage["status"], "PARTIAL")
|
|
53
|
+
self.assertEqual(coverage["eligible_code_files"], 1)
|
|
54
|
+
self.assertIn("Cobertura parcial", " ".join(context["system_model"]["unknowns"]))
|
|
55
|
+
|
|
56
|
+
def test_nested_manifests_detect_web_stacks(self):
|
|
57
|
+
with TemporaryDirectory() as directory:
|
|
58
|
+
root = Path(directory)
|
|
59
|
+
(root / "api").mkdir()
|
|
60
|
+
(root / "api" / "service.csproj").write_text('<Project Sdk="Microsoft.NET.Sdk.Web" />')
|
|
61
|
+
stack, evidence = detect_project(root)
|
|
62
|
+
self.assertIn("C#", stack["languages"])
|
|
63
|
+
self.assertIn("ASP.NET", stack["frameworks"])
|
|
64
|
+
self.assertEqual(evidence[0].path, "api/service.csproj")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
unittest.main()
|
|
@@ -71,6 +71,8 @@ def test_portal_has_dashboard_and_real_views(tmp_path):
|
|
|
71
71
|
assert "data-view='flujos'" in page
|
|
72
72
|
assert "data-go='evidencia'" in page
|
|
73
73
|
assert "Copiar contexto" in page
|
|
74
|
+
assert "data-go='entradas'" in page
|
|
75
|
+
assert "data-node=" in page or "map-empty" in page
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
def test_cli_and_portal_ignore_scalar_stack_metadata(tmp_path, monkeypatch, capsys):
|
|
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.3.2 → klapcontext-0.4.0}/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
|