klapcontext 0.1.2__tar.gz → 0.1.3__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.
Files changed (30) hide show
  1. {klapcontext-0.1.2 → klapcontext-0.1.3}/PKG-INFO +7 -2
  2. {klapcontext-0.1.2 → klapcontext-0.1.3}/README.md +6 -1
  3. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/__init__.py +1 -1
  4. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/cli.py +6 -4
  5. klapcontext-0.1.3/src/klapcontext/context_builder.py +113 -0
  6. klapcontext-0.1.3/src/klapcontext/evidence.py +39 -0
  7. klapcontext-0.1.3/src/klapcontext/frameworks/laravel.py +138 -0
  8. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/portal.py +7 -1
  9. klapcontext-0.1.3/src/klapcontext/providers/__init__.py +6 -0
  10. klapcontext-0.1.3/src/klapcontext/providers/base.py +14 -0
  11. klapcontext-0.1.3/src/klapcontext/providers/graphify.py +22 -0
  12. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext.egg-info/PKG-INFO +7 -2
  13. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext.egg-info/SOURCES.txt +3 -0
  14. {klapcontext-0.1.2 → klapcontext-0.1.3}/tests/test_klapcontext.py +21 -0
  15. klapcontext-0.1.2/src/klapcontext/context_builder.py +0 -72
  16. klapcontext-0.1.2/src/klapcontext/evidence.py +0 -11
  17. klapcontext-0.1.2/src/klapcontext/frameworks/laravel.py +0 -74
  18. {klapcontext-0.1.2 → klapcontext-0.1.3}/LICENSE +0 -0
  19. {klapcontext-0.1.2 → klapcontext-0.1.3}/pyproject.toml +0 -0
  20. {klapcontext-0.1.2 → klapcontext-0.1.3}/setup.cfg +0 -0
  21. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/agent_context.py +0 -0
  22. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/detector.py +0 -0
  23. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/frameworks/__init__.py +0 -0
  24. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/git.py +0 -0
  25. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/graphify.py +0 -0
  26. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext/system.py +0 -0
  27. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext.egg-info/dependency_links.txt +0 -0
  28. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext.egg-info/entry_points.txt +0 -0
  29. {klapcontext-0.1.2 → klapcontext-0.1.3}/src/klapcontext.egg-info/requires.txt +0 -0
  30. {klapcontext-0.1.2 → klapcontext-0.1.3}/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.1.2
3
+ Version: 0.1.3
4
4
  Summary: Make your repository understandable to humans and AI.
5
5
  Author: Willy Cordon
6
6
  License-Expression: Apache-2.0
@@ -29,7 +29,12 @@ Dynamic: license-file
29
29
 
30
30
  # KlapContext
31
31
 
32
- Make your repository understandable to humans and AI.
32
+ **Understand an unfamiliar codebase before changing it.**
33
+
34
+ KlapContext convierte evidencia de código en un modelo del sistema: puntos de
35
+ entrada, flujos, automatizaciones, integraciones y sus incertidumbres.
36
+ Graphify funciona como proveedor de análisis técnico; KlapContext es la capa
37
+ que transforma esa evidencia en comprensión para personas y agentes de IA.
33
38
 
34
39
  KlapContext convierte un repositorio en contexto de ingeniería reutilizable: un
35
40
  portal estático para personas y un briefing compacto para agentes de IA. Usa
@@ -1,6 +1,11 @@
1
1
  # KlapContext
2
2
 
3
- Make your repository understandable to humans and AI.
3
+ **Understand an unfamiliar codebase before changing it.**
4
+
5
+ KlapContext convierte evidencia de código en un modelo del sistema: puntos de
6
+ entrada, flujos, automatizaciones, integraciones y sus incertidumbres.
7
+ Graphify funciona como proveedor de análisis técnico; KlapContext es la capa
8
+ que transforma esa evidencia en comprensión para personas y agentes de IA.
4
9
 
5
10
  KlapContext convierte un repositorio en contexto de ingeniería reutilizable: un
6
11
  portal estático para personas y un briefing compacto para agentes de IA. Usa
@@ -1,3 +1,3 @@
1
1
  """KlapContext local engineering-context generator."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -6,18 +6,20 @@ from . import __version__
6
6
  from .agent_context import write as write_agent
7
7
  from .context_builder import build
8
8
  from .git import commit, dirty_files, exclude_klap, is_repository
9
- from .graphify import GraphifyError, copy_outputs, generate, load_graph, version
9
+ from .graphify import GraphifyError
10
10
  from .portal import write as write_portal
11
+ from .providers import default_provider
11
12
 
12
13
  def root_path(value: str | None) -> Path: return Path(value or os.getcwd()).resolve()
13
14
 
14
15
  def generate_all(root: Path, update: bool=False) -> dict:
15
16
  if not is_repository(root): raise RuntimeError(f"Not a Git repository: {root}")
16
17
  exclude_klap(root); klap=root/".klap"; klap.mkdir(exist_ok=True)
17
- graph_path=generate(root, update); files=copy_outputs(root, klap/"graphify")
18
- context=build(root, load_graph(graph_path)); (klap/"context.json").write_text(json.dumps(context, indent=2)+"\n", encoding="utf-8")
18
+ provider=default_provider()
19
+ graph_path=provider.generate(root, update); files=provider.copy_outputs(root, klap/"graphify")
20
+ context=build(root, provider.load_graph(graph_path)); (klap/"context.json").write_text(json.dumps(context, indent=2)+"\n", encoding="utf-8")
19
21
  agent=write_agent(context, klap/"agent-context.md"); write_portal(context, agent, files, klap/"index.html")
20
- state={"schema_version":"0.1","generated_at":datetime.now(timezone.utc).isoformat(),"git_commit":commit(root),"graphify_version":version(),"klap_version":__version__}
22
+ state={"schema_version":"0.2","generated_at":datetime.now(timezone.utc).isoformat(),"git_commit":commit(root),"provider":{"name":provider.name,"version":provider.version()},"klap_version":__version__}
21
23
  (klap/"state.json").write_text(json.dumps(state, indent=2)+"\n", encoding="utf-8")
22
24
  return context
23
25
 
@@ -0,0 +1,113 @@
1
+ """Build the product-level system model from deterministic evidence providers."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from .detector import detect_project
9
+ from .frameworks.laravel import analyze as analyze_laravel
10
+ from .git import commit
11
+ from .system import deployment, documents, entry_points, external_systems, purpose
12
+
13
+
14
+ def _nodes(graph: dict) -> list[dict]:
15
+ return graph.get("nodes", []) if isinstance(graph.get("nodes"), list) else graph.get("graph", {}).get("nodes", [])
16
+
17
+
18
+ def _modules(nodes: list[dict]) -> list[dict]:
19
+ return [{"name": node.get("label") or node.get("name") or node.get("id"), "path": node.get("file") or node.get("path"), "kind": node.get("type", "module"), "description": None, "status": "CONFIRMED", "evidence": []} for node in nodes if (node.get("label") or node.get("name") or node.get("id")) and (node.get("file") or node.get("path"))][:100]
20
+
21
+
22
+ def _runtime(points: list[dict], background: list[dict], frameworks: list[str]) -> dict:
23
+ modes, descriptions = [], []
24
+ if any(point["type"] == "http" for point in points): modes.append("http"); descriptions.append("Las solicitudes HTTP ingresan por las rutas detectadas.")
25
+ if any(point["type"] in {"cli", "artisan_command"} for point in points): modes.append("cli"); descriptions.append("La aplicación expone comandos ejecutables.")
26
+ if background: modes.append("scheduler"); descriptions.append("La aplicación declara procesamiento en segundo plano.")
27
+ return {"modes": modes, "layers": frameworks, "description": " ".join(descriptions) or None, "status": "CONFIRMED" if descriptions else "UNKNOWN"}
28
+
29
+
30
+ def _io(points: list[dict], systems: list[dict], stores: list[dict]) -> tuple[list[dict], list[dict]]:
31
+ inputs, outputs = [], []
32
+ if any(point["type"] == "http" for point in points):
33
+ inputs.append({"type": "http_request", "description": "Solicitudes HTTP", "status": "CONFIRMED"})
34
+ outputs.append({"type": "http_response", "description": "Respuestas HTTP", "status": "INFERRED"})
35
+ if any(point["type"] in {"cli", "artisan_command"} for point in points): inputs.append({"type": "cli", "description": "Invocación por línea de comandos", "status": "CONFIRMED"})
36
+ if stores:
37
+ inputs.append({"type": "datastore", "description": "Base de datos", "status": "INFERRED"})
38
+ outputs.append({"type": "datastore", "description": "Escrituras de base de datos", "status": "INFERRED"})
39
+ return inputs, outputs
40
+
41
+
42
+ def _flows(root: Path, laravel: dict, stores: list[dict], systems: list[dict]) -> list[dict]:
43
+ flows = []
44
+ for route in laravel["routes"][:8]:
45
+ steps = [{"name": route["name"], "kind": "route", "path": route["source"], "status": "CONFIRMED"}]
46
+ if route.get("target"):
47
+ steps.append({"name": route["target"], "kind": "controller", "path": route["source"], "status": "CONFIRMED"})
48
+ controller = root / "app" / "Http" / "Controllers" / (route["target"].split("::")[0] + ".php")
49
+ if controller.exists():
50
+ names = re.findall(r"\b(\w*(?:Service|Repository))\b", controller.read_text(encoding="utf-8", errors="replace"))
51
+ if names: steps.append({"name": names[0], "kind": "service", "path": str(controller.relative_to(root)), "status": "INFERRED"})
52
+ if stores: steps.append({"name": stores[0]["name"], "kind": "datastore", "path": None, "status": "INFERRED"})
53
+ elif systems: steps.append({"name": systems[0]["name"], "kind": "external_system", "path": None, "status": "INFERRED"})
54
+ if len(steps) > 1:
55
+ status = "CONFIRMED" if all(step["status"] == "CONFIRMED" for step in steps) else "INFERRED"
56
+ flows.append({"name": route["name"], "trigger": "http", "entry_point": route["name"], "steps": steps, "status": status, "confidence": 1.0 if status == "CONFIRMED" else .7, "evidence": route["evidence"], "inputs": [], "outputs": [], "external_systems": [], "datastores": [], "tests": []})
57
+ for command in laravel["scheduled_processes"][:3]:
58
+ flows.append({"name": command["name"], "trigger": "scheduler", "entry_point": command["name"], "steps": [{"name": "Scheduler", "kind": "scheduler", "path": command["path"], "status": "CONFIRMED"}, {"name": command["name"], "kind": "command", "path": command["path"], "status": "CONFIRMED"}], "status": "CONFIRMED", "confidence": 1.0, "evidence": command["evidence"], "inputs": [], "outputs": [], "external_systems": [], "datastores": [], "tests": []})
59
+ return flows[:10]
60
+
61
+
62
+ def _interactions(flows: list[dict], systems: list[dict], stores: list[dict]) -> dict:
63
+ nodes, edges = [], []
64
+ def add_node(name, kind):
65
+ if not any(item["name"] == name for item in nodes): nodes.append({"name": name, "type": kind})
66
+ for flow in flows:
67
+ previous = None
68
+ for step in flow["steps"]:
69
+ add_node(step["name"], step["kind"])
70
+ if previous: edges.append({"source": previous, "target": step["name"], "label": "uses" if step["kind"] in {"datastore", "external_system"} else "calls"})
71
+ previous = step["name"]
72
+ for store in stores: add_node(store["name"], "datastore")
73
+ for system in systems: add_node(system["name"], "external_system")
74
+ return {"nodes": nodes[:30], "edges": edges[:45], "status": "INFERRED" if edges else "UNKNOWN"}
75
+
76
+
77
+ def _start(root: Path, laravel: dict, systems: list[dict]) -> list[dict]:
78
+ result = []
79
+ for path, intent in (("routes/api.php", "Entender la API"), ("routes/web.php", "Entender las rutas web"), ("routes/console.php", "Entender procesamiento programado"), ("composer.json", "Entender dependencias"), ("config/database.php", "Entender acceso a datos")):
80
+ if (root / path).exists(): result.append({"intent": intent, "paths": [path], "reason": "Archivo Laravel relevante detectado"})
81
+ if systems: result.append({"intent": "Entender integraciones", "paths": sorted({path for system in systems for path in system["configuration"]}), "reason": "Sistemas externos detectados"})
82
+ return result
83
+
84
+
85
+ def _analysis_evidence(laravel: dict) -> list[dict]:
86
+ seen, result = set(), []
87
+ for group in ("routes", "commands", "scheduled_processes", "queue_jobs", "events", "listeners", "components", "dependencies"):
88
+ for item in laravel.get(group, []):
89
+ for evidence in item.get("evidence", []):
90
+ key = (evidence.get("path"), evidence.get("line"), evidence.get("reason"), evidence.get("symbol"))
91
+ if key not in seen: seen.add(key); result.append(evidence)
92
+ return result
93
+
94
+
95
+ def build(root: Path, graph: dict) -> dict:
96
+ stack, detector_evidence = detect_project(root)
97
+ docs = documents(root); value = purpose(root, docs)
98
+ systems, stores = external_systems(root); deploy, observability, _ = deployment(root)
99
+ empty_laravel = {"routes": [], "commands": [], "scheduled_processes": [], "queue_jobs": [], "events": [], "listeners": [], "components": [], "dependencies": [], "runtime_surfaces": [], "important_files": [], "framework": None}
100
+ laravel = analyze_laravel(root) if "Laravel" in stack["frameworks"] else empty_laravel
101
+ generic_points, generic_background = entry_points(root)
102
+ points = laravel["routes"] + laravel["commands"] + laravel["queue_jobs"] + laravel["listeners"] or generic_points
103
+ background = laravel["scheduled_processes"] + laravel["queue_jobs"] or generic_background
104
+ runtime = _runtime(points, background, stack["frameworks"])
105
+ inputs, outputs = _io(points, systems, stores)
106
+ flows = _flows(root, laravel, stores, systems) if laravel["routes"] else []
107
+ important = laravel["important_files"] or ([{"path": "README.md", "role": "Documentación", "reason": "README detectado"}] if (root / "README.md").exists() else [])
108
+ tests = [{"path": path, "reason": "Tests detectados"} for path in ("tests", "test", "phpunit.xml", "pytest.ini") if (root / path).exists()]
109
+ 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)
110
+ system = {"purpose": value, "project_story": {"text": story or None, "status": "CONFIRMED" if story else "UNKNOWN"}, "runtime": runtime, "framework": laravel["framework"], "runtime_surfaces": laravel["runtime_surfaces"], "routes": laravel["routes"], "commands": laravel["commands"], "scheduled_processes": laravel["scheduled_processes"], "queue_jobs": laravel["queue_jobs"], "events": laravel["events"], "listeners": laravel["listeners"], "components": laravel["components"], "dependencies": laravel["dependencies"], "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": _start(root, laravel, systems), "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]}
111
+ technical = {"languages": stack["languages"], "frameworks": stack["frameworks"], "framework": laravel["framework"], "dependencies": laravel["dependencies"], "graph": {"nodes": len(_nodes(graph)), "edges": len(graph.get("edges", graph.get("graph", {}).get("edges", [])))}, "documents": [{"path": doc["path"]} for doc in docs], "tests": tests, "infrastructure": stack["infrastructure"]}
112
+ 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)}
113
+ return {"schema_version": "0.4", "project": project, "technical_model": technical, "system_model": system, "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": laravel["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(laravel)}
@@ -0,0 +1,39 @@
1
+ """Small, serializable evidence records shared by every analyzer."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Evidence:
10
+ source_type: str
11
+ path: str
12
+ reason: str
13
+ status: str = "CONFIRMED"
14
+ confidence: float = 1.0
15
+ line: int | None = None
16
+ symbol: str | None = None
17
+ snippet: str | None = None
18
+
19
+ def as_dict(self) -> dict:
20
+ return asdict(self)
21
+
22
+
23
+ def from_match(root: Path, path: Path, match_start: int, reason: str, *, symbol: str | None = None, status: str = "CONFIRMED", confidence: float | None = None) -> Evidence:
24
+ """Create evidence anchored to one source line; snippets never include env files."""
25
+ text = path.read_text(encoding="utf-8", errors="replace")
26
+ line = text.count("\n", 0, match_start) + 1
27
+ snippet = None
28
+ if not path.name.startswith(".env"):
29
+ snippet = text.splitlines()[line - 1].strip()[:240]
30
+ return Evidence(
31
+ "source",
32
+ str(path.relative_to(root)),
33
+ reason,
34
+ status,
35
+ confidence if confidence is not None else (1.0 if status == "CONFIRMED" else 0.7),
36
+ line,
37
+ symbol,
38
+ snippet,
39
+ )
@@ -0,0 +1,138 @@
1
+ """Deterministic Laravel conventions analyzer; Graphify remains the code graph."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from ..evidence import Evidence, from_match
9
+
10
+
11
+ def _read(path: Path) -> str:
12
+ try:
13
+ return path.read_text(encoding="utf-8", errors="replace")
14
+ except OSError:
15
+ return ""
16
+
17
+
18
+ def _symbol(target: str) -> str | None:
19
+ match = re.search(r"(?:\[\s*)?([A-Za-z_]\w*(?:\\[A-Za-z_]\w*)*)::class\s*,\s*['\"](\w+)['\"]", target)
20
+ return f"{match.group(1).split('\\')[-1]}::{match.group(2)}" if match else None
21
+
22
+
23
+ def _e(root: Path, path: Path, offset: int, reason: str, *, symbol: str | None = None, status: str = "CONFIRMED") -> list[dict]:
24
+ return [from_match(root, path, offset, reason, symbol=symbol, status=status).as_dict()]
25
+
26
+
27
+ def _category(package: str) -> str:
28
+ package = package.lower()
29
+ if package == "laravel/framework": return "framework"
30
+ if any(token in package for token in ("mysql", "postgres", "mongodb", "redis", "doctrine/dbal")): return "database"
31
+ if any(token in package for token in ("queue", "horizon", "rabbit", "kafka")): return "queue"
32
+ if any(token in package for token in ("jwt", "sanctum", "passport", "auth")): return "authentication"
33
+ if any(token in package for token in ("guzzle", "http", "curl", "soap")): return "http"
34
+ if any(token in package for token in ("sentry", "telescope", "prometheus", "opentelemetry")): return "observability"
35
+ if any(token in package for token in ("phpunit", "pest", "faker")): return "testing"
36
+ if any(token in package for token in ("flysystem", "s3", "filesystem")): return "storage"
37
+ return "other"
38
+
39
+
40
+ def _composer(root: Path) -> tuple[list[dict], dict]:
41
+ composer = root / "composer.json"
42
+ if not composer.exists():
43
+ return [], {"laravel": None, "php": None}
44
+ try:
45
+ data = json.loads(_read(composer))
46
+ except json.JSONDecodeError:
47
+ return [], {"laravel": None, "php": None}
48
+ dependencies = []
49
+ for scope, key in (("runtime", "require"), ("development", "require-dev")):
50
+ for package, version in data.get(key, {}).items():
51
+ evidence = Evidence("manifest", "composer.json", f"Dependencia Composer ({scope})", "CONFIRMED", 1.0, symbol=package).as_dict()
52
+ dependencies.append({"package": package, "version": version, "scope": scope, "category": _category(package), "status": "CONFIRMED", "evidence": [evidence]})
53
+ requirements = data.get("require", {})
54
+ return dependencies, {"laravel": requirements.get("laravel/framework"), "php": requirements.get("php")}
55
+
56
+
57
+ def _routes(root: Path) -> tuple[list[dict], list[dict]]:
58
+ routes, scheduled = [], []
59
+ directory = root / "routes"
60
+ if not directory.is_dir():
61
+ return routes, scheduled
62
+ route_pattern = re.compile(r"Route::(get|post|put|patch|delete|options|any)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*(.+?)\);", re.S | re.I)
63
+ resource_pattern = re.compile(r"Route::(?:api)?resource\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z_]\w*(?:\\[A-Za-z_]\w*)*)::class", re.I)
64
+ schedule_pattern = re.compile(r"Schedule::(?:command|job)\s*\(\s*['\"]([^'\"]+)['\"]\s*\).*?->([A-Za-z]\w*)\s*\(", re.S)
65
+ for file in directory.glob("*.php"):
66
+ text = _read(file)
67
+ for match in route_pattern.finditer(text):
68
+ method, uri, target = match.groups()
69
+ handler = _symbol(target)
70
+ name = f"{method.upper()} /{uri.lstrip('/')}"
71
+ routes.append({"type": "http", "method": method.upper(), "uri": "/" + uri.lstrip("/"), "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": [], "status": "CONFIRMED", "evidence": _e(root, file, match.start(), "Declaración de ruta Laravel", symbol=name)})
72
+ for match in resource_pattern.finditer(text):
73
+ resource, controller = match.groups()
74
+ name = f"RESOURCE /{resource.lstrip('/')}"
75
+ routes.append({"type": "http", "method": "RESOURCE", "uri": "/" + resource.lstrip("/"), "name": name, "path": str(file.relative_to(root)), "source": str(file.relative_to(root)), "line": text.count("\n", 0, match.start()) + 1, "target": controller.split("\\")[-1], "handler": controller.split("\\")[-1], "middleware": [], "status": "INFERRED", "evidence": _e(root, file, match.start(), "Declaración resource de Laravel", symbol=name, status="INFERRED")})
76
+ if file.name == "console.php":
77
+ for match in schedule_pattern.finditer(text):
78
+ command, frequency = match.groups()
79
+ scheduled.append({"name": command, "type": "scheduled_command", "schedule": frequency, "path": str(file.relative_to(root)), "line": text.count("\n", 0, match.start()) + 1, "status": "CONFIRMED", "evidence": _e(root, file, match.start(), "Comando programado de Laravel", symbol=command)})
80
+ return routes, scheduled
81
+
82
+
83
+ def _scheduled_kernel(root: Path) -> list[dict]:
84
+ kernel = root / "app" / "Console" / "Kernel.php"
85
+ if not kernel.exists():
86
+ return []
87
+ text = _read(kernel)
88
+ pattern = re.compile(r"\$schedule->(?:command|job)\s*\(\s*['\"]([^'\"]+)['\"]\s*\).*?->([A-Za-z]\w*)\s*\(", re.S)
89
+ return [{"name": match.group(1), "type": "scheduled_command", "schedule": match.group(2), "path": str(kernel.relative_to(root)), "line": text.count("\n", 0, match.start()) + 1, "status": "CONFIRMED", "evidence": _e(root, kernel, match.start(), "Scheduler de Laravel", symbol=match.group(1))} for match in pattern.finditer(text)]
90
+
91
+
92
+ def _classes(root: Path, folder: str, kind: str, predicate=None) -> list[dict]:
93
+ directory = root / folder
94
+ if not directory.is_dir():
95
+ return []
96
+ result = []
97
+ for file in directory.rglob("*.php"):
98
+ text = _read(file)
99
+ if predicate and not predicate(text):
100
+ continue
101
+ name = file.stem
102
+ match = re.search(r"\bclass\s+(\w+)", text)
103
+ if match:
104
+ name = match.group(1)
105
+ result.append({"name": name, "type": kind, "path": str(file.relative_to(root)), "line": text.count("\n", 0, match.start()) + 1 if match else 1, "status": "CONFIRMED", "evidence": _e(root, file, match.start() if match else 0, f"Clase Laravel de tipo {kind}", symbol=name)})
106
+ return result
107
+
108
+
109
+ def analyze(root: Path) -> dict:
110
+ routes, scheduled = _routes(root)
111
+ scheduled.extend(_scheduled_kernel(root))
112
+ dependencies, versions = _composer(root)
113
+ commands = _classes(root, "app/Console/Commands", "artisan_command")
114
+ for command in commands:
115
+ text = _read(root / command["path"])
116
+ signature = re.search(r"(?:protected|public)\s+\$signature\s*=\s*['\"]([^'\"]+)", text)
117
+ if signature:
118
+ command["name"] = signature.group(1)
119
+ command["evidence"] = _e(root, root / command["path"], signature.start(), "Firma de comando Artisan", symbol=command["name"])
120
+ jobs = _classes(root, "app/Jobs", "queue_job", lambda text: "ShouldQueue" in text)
121
+ for job in jobs:
122
+ text = _read(root / job["path"])
123
+ queue = re.search(r"(?:public|protected)\s+\$queue\s*=\s*['\"]([^'\"]+)", text)
124
+ job["queue"] = queue.group(1) if queue else None
125
+ events = _classes(root, "app/Events", "event")
126
+ listeners = _classes(root, "app/Listeners", "listener")
127
+ components = []
128
+ for folder, kind in (("app/Http/Controllers", "controller"), ("app/Http/Middleware", "middleware"), ("app/Services", "service"), ("app/Repositories", "repository"), ("app/Models", "model"), ("database/migrations", "migration"), ("database/seeders", "seeder")):
129
+ components.extend(_classes(root, folder, kind))
130
+ important = []
131
+ for path, role in (("routes/api.php", "Rutas HTTP API"), ("routes/web.php", "Rutas web"), ("routes/console.php", "Programación de comandos"), ("routes/channels.php", "Canales de broadcast"), ("app/Console/Kernel.php", "Scheduler Laravel"), ("config/database.php", "Configuración de datos"), ("config/services.php", "Configuración de integraciones"), (".env.example", "Variables de entorno documentadas"), ("composer.json", "Dependencias PHP")):
132
+ if (root / path).exists():
133
+ important.append({"path": path, "role": role, "reason": f"{role} detectado"})
134
+ surfaces = []
135
+ for kind, label, values in (("http", "HTTP", routes), ("cli", "CLI", commands), ("scheduler", "Scheduler", scheduled), ("queue", "Queue", jobs), ("event", "Eventos", events), ("listener", "Listeners", listeners)):
136
+ if values:
137
+ surfaces.append({"type": kind, "label": label, "count": len(values), "items": values})
138
+ return {"framework": {"name": "Laravel", "version": versions["laravel"], "php_version": versions["php"], "status": "CONFIRMED", "evidence": [Evidence("manifest", "composer.json", "laravel/framework declarado", "CONFIRMED", 1.0, symbol="laravel/framework").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}
@@ -69,10 +69,16 @@ def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
69
69
  evidence = context.get("evidence", [])
70
70
  surfaces = system.get("runtime_surfaces", [])
71
71
  unknowns = system.get("unknowns", [])
72
+ framework = system.get("framework") or {}
72
73
  purpose = system.get("purpose", {}).get("text") or "No pudimos determinar el propósito con suficiente confianza."
73
74
  story = system.get("project_story", {}).get("text") or system.get("runtime", {}).get("description") or "Todavía no hay una narrativa técnica verificable."
74
75
  state = "ACTUAL" if project.get("git_commit") else "SIN COMMIT"
75
- tags = "".join(f"<span>{e(item)}</span>" for item in stack[:6]) or "<span>Stack no identificado</span>"
76
+ tags = "".join(f"<span>{e(item)}</span>" for item in stack[:6])
77
+ if framework.get("version"):
78
+ tags += f"<span>{e(framework.get('name', 'Framework'))} {e(framework['version'])}</span>"
79
+ if framework.get("php_version"):
80
+ tags += f"<span>PHP {e(framework['php_version'])}</span>"
81
+ tags = tags or "<span>Stack no identificado</span>"
76
82
  surface_cards = "".join(
77
83
  f"<article class='surface'><b>{e(item.get('count', 0))}</b><span>{e(item.get('label', 'superficies'))}</span></article>"
78
84
  for item in surfaces
@@ -0,0 +1,6 @@
1
+ """Built-in evidence providers. More engines can implement AnalysisProvider later."""
2
+ from .graphify import GraphifyProvider
3
+
4
+
5
+ def default_provider() -> GraphifyProvider:
6
+ return GraphifyProvider()
@@ -0,0 +1,14 @@
1
+ """Contract for code-analysis engines feeding KlapContext evidence."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+
8
+ class AnalysisProvider(Protocol):
9
+ name: str
10
+
11
+ def generate(self, root: Path, update: bool = False) -> Path: ...
12
+ def load_graph(self, graph: Path) -> dict: ...
13
+ def copy_outputs(self, root: Path, destination: Path) -> list[str]: ...
14
+ def version(self) -> str | None: ...
@@ -0,0 +1,22 @@
1
+ """Graphify provider adapter. Keeps the engine separate from the system model."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from .. import graphify
7
+
8
+
9
+ class GraphifyProvider:
10
+ name = "graphify"
11
+
12
+ def generate(self, root: Path, update: bool = False) -> Path:
13
+ return graphify.generate(root, update)
14
+
15
+ def load_graph(self, graph: Path) -> dict:
16
+ return graphify.load_graph(graph)
17
+
18
+ def copy_outputs(self, root: Path, destination: Path) -> list[str]:
19
+ return graphify.copy_outputs(root, destination)
20
+
21
+ def version(self) -> str | None:
22
+ return graphify.version()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: klapcontext
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Make your repository understandable to humans and AI.
5
5
  Author: Willy Cordon
6
6
  License-Expression: Apache-2.0
@@ -29,7 +29,12 @@ Dynamic: license-file
29
29
 
30
30
  # KlapContext
31
31
 
32
- Make your repository understandable to humans and AI.
32
+ **Understand an unfamiliar codebase before changing it.**
33
+
34
+ KlapContext convierte evidencia de código en un modelo del sistema: puntos de
35
+ entrada, flujos, automatizaciones, integraciones y sus incertidumbres.
36
+ Graphify funciona como proveedor de análisis técnico; KlapContext es la capa
37
+ que transforma esa evidencia en comprensión para personas y agentes de IA.
33
38
 
34
39
  KlapContext convierte un repositorio en contexto de ingeniería reutilizable: un
35
40
  portal estático para personas y un briefing compacto para agentes de IA. Usa
@@ -19,4 +19,7 @@ src/klapcontext.egg-info/requires.txt
19
19
  src/klapcontext.egg-info/top_level.txt
20
20
  src/klapcontext/frameworks/__init__.py
21
21
  src/klapcontext/frameworks/laravel.py
22
+ src/klapcontext/providers/__init__.py
23
+ src/klapcontext/providers/base.py
24
+ src/klapcontext/providers/graphify.py
22
25
  tests/test_klapcontext.py
@@ -66,3 +66,24 @@ def test_laravel_adapter_detects_flow_and_map(tmp_path):
66
66
  assert system["scheduled_processes"][0]["schedule"] == "hourly"
67
67
  assert system["queue_jobs"][0]["queue"] == "reports"
68
68
  assert system["main_flows"] and system["system_interactions"]["nodes"]
69
+
70
+
71
+ def test_laravel_analysis_includes_versions_components_and_navigable_evidence(tmp_path):
72
+ root = fixture_repo(tmp_path)
73
+ (root / "composer.json").write_text(json.dumps({"require": {"php": "^8.2", "laravel/framework": "^11.0", "guzzlehttp/guzzle": "^7.0", "laravel/sanctum": "^4.0"}, "require-dev": {"pestphp/pest": "^3.0"}}))
74
+ (root / "routes" / "api.php").write_text("<?php\nRoute::post('/orders', [OrderController::class, 'store']);\n")
75
+ for folder, name in (("app/Http/Controllers", "OrderController"), ("app/Services", "OrderService"), ("app/Repositories", "OrderRepository"), ("app/Models", "Order"), ("app/Events", "OrderCreated"), ("app/Listeners", "SyncOrder")):
76
+ path = root / folder; path.mkdir(parents=True, exist_ok=True)
77
+ (path / f"{name}.php").write_text(f"<?php class {name} {{}}")
78
+ context = build(root, {"nodes": []})
79
+ system = context["system_model"]
80
+ assert system["framework"]["version"] == "^11.0"
81
+ assert system["framework"]["php_version"] == "^8.2"
82
+ assert {item["category"] for item in system["dependencies"]} >= {"framework", "http", "authentication", "testing"}
83
+ assert {item["type"] for item in system["components"]} >= {"controller", "service", "repository", "model"}
84
+ route = system["routes"][0]
85
+ assert route["line"] == 2 and route["evidence"][0]["line"] == 2
86
+ assert route["evidence"][0]["snippet"] == "Route::post('/orders', [OrderController::class, 'store']);"
87
+ assert len(system["events"]) == 1 and len(system["listeners"]) == 1
88
+ page = render_portal(context, render_agent(context), [])
89
+ assert "Laravel ^11.0" in page and "PHP ^8.2" in page
@@ -1,72 +0,0 @@
1
- from __future__ import annotations
2
- from datetime import datetime, timezone
3
- from pathlib import Path
4
- from .detector import detect_project
5
- from .evidence import Evidence
6
- from .frameworks.laravel import analyze as analyze_laravel
7
- from .git import commit
8
- from .system import deployment, documents, entry_points, external_systems, purpose
9
-
10
- def _nodes(graph): return graph.get("nodes",[]) if isinstance(graph.get("nodes"),list) else graph.get("graph",{}).get("nodes",[])
11
- def _modules(nodes):
12
- return [{"name":n.get("label") or n.get("name") or n.get("id"),"path":n.get("file") or n.get("path"),"kind":n.get("type","module"),"description":None,"status":"CONFIRMED","evidence":[]} for n in nodes if (n.get("label") or n.get("name") or n.get("id")) and (n.get("file") or n.get("path"))][:100]
13
- def _runtime(points, background, framework):
14
- modes=[]; text=[]
15
- if any(p["type"]=="http" for p in points): modes.append("http"); text.append("Las solicitudes HTTP ingresan por las rutas detectadas.")
16
- if any(p["type"] in {"cli","artisan_command"} for p in points): modes.append("cli"); text.append("La aplicación expone comandos ejecutables.")
17
- if background: modes.append("scheduler"); text.append("La aplicación declara procesamiento en segundo plano.")
18
- return {"modes":modes,"layers":framework,"description":" ".join(text) or None,"status":"CONFIRMED" if text else "UNKNOWN"}
19
- def _io(points, systems, stores):
20
- inputs=[]; outputs=[]
21
- if any(p["type"]=="http" for p in points): inputs.append({"type":"http_request","description":"Solicitudes HTTP","status":"CONFIRMED"}); outputs.append({"type":"http_response","description":"Respuestas HTTP","status":"INFERRED"})
22
- if any(p["type"] in {"cli","artisan_command"} for p in points): inputs.append({"type":"cli","description":"Invocación por línea de comandos","status":"CONFIRMED"})
23
- if stores: inputs.append({"type":"datastore","description":"Base de datos","status":"INFERRED"}); outputs.append({"type":"datastore","description":"Escrituras de base de datos","status":"INFERRED"})
24
- return inputs,outputs
25
- def _flows(root, laravel, stores, systems):
26
- flows=[]
27
- for route in laravel["routes"][:8]:
28
- steps=[{"name":route["name"],"kind":"route","path":route["source"]}]
29
- if route.get("target"):
30
- steps.append({"name":route["target"],"kind":"controller","path":route["source"]})
31
- ctrl=root/"app"/"Http"/"Controllers"/(route["target"].split("::")[0]+".php")
32
- if ctrl.exists():
33
- import re
34
- names=re.findall(r"\b(\w*(?:Service|Repository))\b",ctrl.read_text(encoding="utf-8",errors="replace"))
35
- if names: steps.append({"name":names[0],"kind":"service","path":None})
36
- if stores: steps.append({"name":stores[0]["name"],"kind":"datastore","path":None})
37
- elif systems: steps.append({"name":systems[0]["name"],"kind":"external_system","path":None})
38
- if len(steps)>1: flows.append({"name":route["name"],"trigger":"http","entry_point":route["name"],"steps":steps,"status":"INFERRED","confidence":None,"evidence":route["evidence"],"inputs":[],"outputs":[],"external_systems":[],"datastores":[],"tests":[]})
39
- for command in laravel["scheduled_processes"][:3]: flows.append({"name":command["name"],"trigger":"scheduler","entry_point":command["name"],"steps":[{"name":"Scheduler","kind":"scheduler","path":command["path"]},{"name":command["name"],"kind":"command","path":command["path"]}],"status":"CONFIRMED","confidence":None,"evidence":command["evidence"],"inputs":[],"outputs":[],"external_systems":[],"datastores":[],"tests":[]})
40
- return flows[:10]
41
- def _interactions(flows, systems, stores):
42
- nodes=[]; edges=[]
43
- def n(name,kind):
44
- if not any(x["name"]==name for x in nodes): nodes.append({"name":name,"type":kind})
45
- for flow in flows:
46
- previous=None
47
- for step in flow["steps"]:
48
- n(step["name"],step["kind"])
49
- if previous: edges.append({"source":previous,"target":step["name"],"label":"uses" if step["kind"] in {"datastore","external_system"} else "calls"})
50
- previous=step["name"]
51
- for x in stores: n(x["name"],"datastore")
52
- for x in systems: n(x["name"],"external_system")
53
- return {"nodes":nodes[:30],"edges":edges[:45],"status":"INFERRED" if edges else "UNKNOWN"}
54
- def _start(root, laravel, systems):
55
- result=[]
56
- for path,intent in (("routes/api.php","Entender la API"),("routes/web.php","Entender las rutas web"),("routes/console.php","Entender procesamiento programado"),("composer.json","Entender dependencias"),("config/database.php","Entender acceso a datos")):
57
- if (root/path).exists(): result.append({"intent":intent,"paths":[path],"reason":"Archivo Laravel relevante detectado"})
58
- if systems: result.append({"intent":"Entender integraciones","paths":sorted({p for s in systems for p in s["configuration"]}),"reason":"Sistemas externos detectados"})
59
- return result
60
- def build(root: Path, graph: dict) -> dict:
61
- stack,evidence=detect_project(root); docs=documents(root); value=purpose(root,docs); systems,stores=external_systems(root); deploy,observability,_=deployment(root)
62
- laravel=analyze_laravel(root) if "Laravel" in stack["frameworks"] else {"routes":[],"commands":[],"scheduled_processes":[],"queue_jobs":[],"dependencies":[],"runtime_surfaces":[],"important_files":[]}
63
- generic_points,generic_background=entry_points(root)
64
- points=laravel["routes"]+laravel["commands"] or generic_points; background=laravel["scheduled_processes"]+laravel["queue_jobs"] or generic_background
65
- runtime=_runtime(points,background,stack["frameworks"]); inputs,outputs=_io(points,systems,stores); flows=_flows(root,laravel,stores,systems) if laravel["routes"] else []
66
- important=laravel["important_files"] or [{"path":"README.md","role":"Documentación","reason":"README detectado"}] if (root/"README.md").exists() else []
67
- tests=[{"path":x,"reason":"Tests detectados"} for x in ("tests","test","phpunit.xml","pytest.ini") if (root/x).exists()]
68
- story=" ".join(x for x in [value["text"],runtime["description"],("La aplicación está contenerizada con Docker." if "Docker" in deploy["tools"] else None)] if x)
69
- system={"purpose":value,"project_story":{"text":story or None,"status":"CONFIRMED" if story else "UNKNOWN"},"runtime":runtime,"runtime_surfaces":laravel["runtime_surfaces"],"routes":laravel["routes"],"commands":laravel["commands"],"scheduled_processes":laravel["scheduled_processes"],"queue_jobs":laravel["queue_jobs"],"dependencies":laravel["dependencies"],"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":_start(root,laravel,systems),"unknowns":[x for x,ok in (("Propósito de negocio del proyecto",value["text"]),("Plataforma de despliegue en producción",deploy["tools"])) if not ok]}
70
- technical={"languages":stack["languages"],"frameworks":stack["frameworks"],"dependencies":laravel["dependencies"],"graph":{"nodes":len(_nodes(graph)),"edges":len(graph.get("edges",graph.get("graph",{}).get("edges",[])))},"documents":[{"path":d["path"]} for d in docs],"tests":tests,"infrastructure":stack["infrastructure"]}
71
- 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)}
72
- return {"schema_version":"0.3","project":project,"technical_model":technical,"system_model":system,"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":laravel["routes"],"jobs":background,"datastores":stores,"external_systems":systems,"deployment":deploy,"observability":observability,"tests":tests,"evidence":[x.as_dict() for x in evidence]+value["evidence"]}
@@ -1,11 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import asdict, dataclass
3
-
4
- @dataclass(frozen=True)
5
- class Evidence:
6
- source_type: str
7
- path: str
8
- reason: str
9
- status: str = "CONFIRMED"
10
- confidence: float = 1.0
11
- def as_dict(self): return asdict(self)
@@ -1,74 +0,0 @@
1
- """Conservative Laravel conventions scanner; Graphify remains the code graph."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import re
6
- from pathlib import Path
7
-
8
- from ..evidence import Evidence
9
-
10
-
11
- def _read(path: Path) -> str:
12
- try: return path.read_text(encoding="utf-8", errors="replace")
13
- except OSError: return ""
14
-
15
-
16
- def _e(path: str, reason: str, status: str="CONFIRMED") -> list[dict]:
17
- return [Evidence("file", path, reason, status, 1.0 if status == "CONFIRMED" else .8).as_dict()]
18
-
19
-
20
- def _symbol(target: str) -> str | None:
21
- target=target.strip()
22
- match=re.search(r"(?:\[\s*)?([A-Za-z_][\w\\]+)::class\s*,\s*['\"](\w+)['\"]", target)
23
- return f"{match.group(1).split('\\')[-1]}::{match.group(2)}" if match else None
24
-
25
-
26
- def analyze(root: Path) -> dict:
27
- routes=[]; commands=[]; scheduled=[]; jobs=[]; dependencies=[]; important=[]
28
- route_dir=root/"routes"
29
- if route_dir.is_dir():
30
- for file in route_dir.glob("*.php"):
31
- text=_read(file); rel=str(file.relative_to(root))
32
- for method, uri, target in re.findall(r"Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*(.+?)\);", text, re.S|re.I):
33
- routes.append({"type":"http","method":method.upper(),"uri":"/"+uri.lstrip("/"),"name":f"{method.upper()} /{uri.lstrip('/')}","target":_symbol(target),"middleware":[],"source":rel,"status":"CONFIRMED","evidence":_e(rel,"Declaración de ruta Laravel")})
34
- for resource, controller in re.findall(r"Route::(?:api)?resource\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z_][\w\\]+)::class", text, re.I):
35
- routes.append({"type":"http","method":"RESOURCE","uri":"/"+resource.lstrip("/"),"name":f"RESOURCE /{resource.lstrip('/')}","target":controller.split("\\")[-1],"middleware":[],"source":rel,"status":"INFERRED","evidence":_e(rel,"Declaración resource de Laravel","INFERRED")})
36
- if file.name == "console.php":
37
- for command, freq in re.findall(r"Schedule::command\s*\(\s*['\"]([^'\"]+)['\"]\s*\).*?->([A-Za-z]\w*)\s*\(", text, re.S):
38
- scheduled.append({"name":command,"type":"scheduled_command","schedule":freq,"path":rel,"status":"CONFIRMED","evidence":_e(rel,"Comando programado de Laravel")})
39
- commands_dir=root/"app"/"Console"/"Commands"
40
- if commands_dir.is_dir():
41
- for file in commands_dir.glob("*.php"):
42
- text=_read(file); signature=re.search(r"(?:protected|public)\s+\$signature\s*=\s*['\"]([^'\"]+)", text)
43
- commands.append({"name":signature.group(1) if signature else file.stem,"type":"artisan_command","path":str(file.relative_to(root)),"status":"CONFIRMED","evidence":_e(str(file.relative_to(root)),"Clase de comando Artisan")})
44
- jobs_dir=root/"app"/"Jobs"
45
- if jobs_dir.is_dir():
46
- for file in jobs_dir.glob("*.php"):
47
- text=_read(file); rel=str(file.relative_to(root))
48
- if "ShouldQueue" in text:
49
- queue=re.search(r"(?:public|protected)\s+\$queue\s*=\s*['\"]([^'\"]+)", text)
50
- jobs.append({"name":file.stem,"type":"queue_job","queue":queue.group(1) if queue else None,"path":rel,"status":"CONFIRMED","evidence":_e(rel,"Job Laravel que implementa ShouldQueue")})
51
- composer=root/"composer.json"
52
- if composer.exists():
53
- try:
54
- data=json.loads(_read(composer))
55
- for scope,key in (("runtime","require"),("development","require-dev")):
56
- for name, version in data.get(key,{}).items(): dependencies.append({"package":name,"version":version,"scope":scope,"category":_category(name),"status":"CONFIRMED","evidence":_e("composer.json",f"Dependencia Composer ({scope})")})
57
- except json.JSONDecodeError: pass
58
- for path,role in (("routes/api.php","Rutas HTTP API"),("routes/web.php","Rutas web"),("routes/console.php","Programación de comandos"),("app/Console/Kernel.php","Scheduler Laravel"),("config/database.php","Configuración de base de datos"),("bootstrap/app.php","Bootstrap de aplicación"),("composer.json","Dependencias PHP")):
59
- if (root/path).exists(): important.append({"path":path,"role":role,"reason":f"{role} detectado"})
60
- surfaces=[]
61
- if routes: surfaces.append({"type":"http","label":"HTTP","count":len(routes),"items":routes})
62
- if commands: surfaces.append({"type":"cli","label":"CLI","count":len(commands),"items":commands})
63
- if scheduled: surfaces.append({"type":"scheduler","label":"Scheduler","count":len(scheduled),"items":scheduled})
64
- if jobs: surfaces.append({"type":"queue","label":"Queue","count":len(jobs),"items":jobs})
65
- return {"routes":routes,"commands":commands,"scheduled_processes":scheduled,"queue_jobs":jobs,"dependencies":dependencies,"runtime_surfaces":surfaces,"important_files":important}
66
-
67
-
68
- def _category(package: str) -> str:
69
- if package == "laravel/framework": return "framework"
70
- if "phpunit" in package or "pest" in package: return "testing"
71
- if "sentry" in package: return "observability"
72
- if "guzzle" in package: return "http"
73
- if "jwt" in package or "sanctum" in package: return "authentication"
74
- return "other"
File without changes
File without changes
File without changes