klapcontext 0.8.2__tar.gz → 0.9.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.
Files changed (67) hide show
  1. {klapcontext-0.8.2/src/klapcontext.egg-info → klapcontext-0.9.0}/PKG-INFO +19 -15
  2. {klapcontext-0.8.2 → klapcontext-0.9.0}/README.md +18 -14
  3. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/__init__.py +1 -1
  4. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/cli.py +36 -7
  5. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/git.py +4 -1
  6. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/graphify.py +24 -6
  7. klapcontext-0.9.0/src/klapcontext/portal.py +109 -0
  8. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/graphify.py +1 -1
  9. klapcontext-0.9.0/src/klapcontext/providers/lifecycle.py +251 -0
  10. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/optional.py +3 -1
  11. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/serena/client.py +12 -1
  12. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/serena/config.py +3 -1
  13. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/services.py +4 -4
  14. {klapcontext-0.8.2 → klapcontext-0.9.0/src/klapcontext.egg-info}/PKG-INFO +19 -15
  15. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext.egg-info/SOURCES.txt +2 -0
  16. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_klapcontext.py +9 -6
  17. klapcontext-0.9.0/tests/test_provider_lifecycle.py +89 -0
  18. klapcontext-0.8.2/src/klapcontext/portal.py +0 -99
  19. {klapcontext-0.8.2 → klapcontext-0.9.0}/LICENSE +0 -0
  20. {klapcontext-0.8.2 → klapcontext-0.9.0}/pyproject.toml +0 -0
  21. {klapcontext-0.8.2 → klapcontext-0.9.0}/setup.cfg +0 -0
  22. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/__init__.py +0 -0
  23. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/capabilities.py +0 -0
  24. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/compiler.py +0 -0
  25. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/impact.py +0 -0
  26. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/intents.py +0 -0
  27. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/models.py +0 -0
  28. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/planner.py +0 -0
  29. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/verification.py +0 -0
  30. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent/workflows.py +0 -0
  31. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent_context.py +0 -0
  32. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/agent_setup.py +0 -0
  33. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/context_builder.py +0 -0
  34. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/detector.py +0 -0
  35. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/evidence.py +0 -0
  36. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/exploration.py +0 -0
  37. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/__init__.py +0 -0
  38. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/base.py +0 -0
  39. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/fastapi.py +0 -0
  40. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/generic_php.py +0 -0
  41. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/laravel.py +0 -0
  42. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/frameworks/node.py +0 -0
  43. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/freshness.py +0 -0
  44. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/__init__.py +0 -0
  45. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/base.py +0 -0
  46. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/code_intelligence.py +0 -0
  47. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/contracts.py +0 -0
  48. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/javascript_intelligence.py +0 -0
  49. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/models.py +0 -0
  50. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/native.py +0 -0
  51. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/python_intelligence.py +0 -0
  52. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/registry.py +0 -0
  53. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/resolver.py +0 -0
  54. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/serena/__init__.py +0 -0
  55. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/providers/serena/adapter.py +0 -0
  56. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/scope.py +0 -0
  57. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/semantic.py +0 -0
  58. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext/system.py +0 -0
  59. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext.egg-info/dependency_links.txt +0 -0
  60. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext.egg-info/entry_points.txt +0 -0
  61. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext.egg-info/requires.txt +0 -0
  62. {klapcontext-0.8.2 → klapcontext-0.9.0}/src/klapcontext.egg-info/top_level.txt +0 -0
  63. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_agent_setup.py +0 -0
  64. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_exploration.py +0 -0
  65. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_provider_architecture.py +0 -0
  66. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_reliability.py +0 -0
  67. {klapcontext-0.8.2 → klapcontext-0.9.0}/tests/test_serena_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: klapcontext
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: Make your repository understandable to humans and AI.
5
5
  Author: Willy Cordon
6
6
  License-Expression: Apache-2.0
@@ -41,7 +41,7 @@ Dynamic: license-file
41
41
 
42
42
  KlapContext convierte evidencia de código en un modelo del sistema: puntos de
43
43
  entrada, flujos, automatizaciones, integraciones y sus incertidumbres.
44
- Graphify puede funcionar como proveedor opcional de análisis técnico;
44
+ Graphify puede funcionar como proveedor estándar de análisis técnico;
45
45
  Serena puede sumar navegación semántica on demand por MCP; KlapContext conserva
46
46
  analizadores locales y transforma toda evidencia mediante contratos
47
47
  desacoplados para personas y agentes de IA.
@@ -69,12 +69,11 @@ Repositorio → KlapContext → Contexto de ingeniería
69
69
 
70
70
  ```bash
71
71
  pip install klapcontext
72
- # opcional, para sumar el grafo Graphify
73
- pip install "klapcontext[graphify]"
74
72
  ```
75
73
 
76
- La instalación base no requiere Graphify. Como alternativa para una CLI
77
- aislada: `pipx install klapcontext` o `pipx install "klapcontext[graphify]"`.
74
+ La instalación base no incorpora proveedores en el intérprete de KlapContext.
75
+ El primer `klap init` los prepara en entornos aislados bajo
76
+ `.klap/provider-runtime/`.
78
77
 
79
78
  ## Primer uso
80
79
 
@@ -89,22 +88,24 @@ bajo `.klap/` y prepara un `AGENTS.md` privado para que los agentes descubran el
89
88
  contexto progresivamente. Ambos se agregan a `.git/info/exclude`; KlapContext
90
89
  nunca modifica tu `.gitignore`.
91
90
 
92
- Graphify se descubre automáticamente por su ejecutable o módulo Python: no hace
93
- falta habilitarlo. Si está disponible, `init` ejecuta una extracción real y usa
91
+ `klap init` prepara Graphify y Serena con versiones fijadas si no encuentra una
92
+ instalación compatible. Si una preparación falla, los analizadores nativos
93
+ siguen generando contexto y el resumen registra el motivo. Graphify ejecuta una extracción real y usa
94
94
  `.klap/graphify-out/graph.json` en `context.json`, el portal y la configuración
95
95
  MCP. Si no está instalado o falla, los analizadores nativos siguen generando un
96
96
  contexto útil y el reporte queda `PARTIAL`, con proveedores detectados,
97
97
  ejecutados y omitidos (incluido el motivo). `context.json.analysis` conserva la
98
- cobertura, trazas y contribuciones combinadas con procedencia. KlapContext no
99
- instala proveedores
100
- durante `init`. Se puede deshabilitar Graphify explícitamente con
98
+ cobertura, trazas y contribuciones combinadas con procedencia. Se puede
99
+ deshabilitar Graphify explícitamente con
101
100
  `.klap/providers.json` y `{"providers":{"graphify":{"enabled":false}}}`.
102
101
 
103
102
  [Serena](https://github.com/oraios/serena) se integra de forma independiente y
104
- opcional. Si `serena` está instalado o configurado, KlapContext descubre sus
103
+ intercambiable. Si falta una instalación compatible, Serena 1.7.0 se prepara
104
+ en el runtime local; KlapContext descubre sus
105
105
  tools reales por MCP y ofrece símbolos, definiciones, referencias,
106
- implementaciones e impacto por consulta. No ejecuta esas búsquedas durante
107
- `klap init`, no instala Serena y nunca llama sus tools de escritura, shell,
106
+ implementaciones e impacto por consulta. Durante `klap init` sólo ejecuta una
107
+ comprobación acotada de overview sobre un archivo elegible para verificar MCP;
108
+ no realiza búsquedas semánticas amplias, y nunca llama sus tools de escritura, shell,
108
109
  onboarding o memorias. Ver [instalación, configuración, seguridad,
109
110
  compatibilidad y límites](docs/SERENA.md).
110
111
 
@@ -142,12 +143,15 @@ klap scope --json
142
143
  | Comando | Descripción |
143
144
  | --- | --- |
144
145
  | `klap init [ruta]` | Genera el contexto inicial. |
145
- | `klap update [ruta]` | Actualiza los proveedores disponibles y el contexto. |
146
+ | `klap init --no-serena [ruta]` | Omite instalación, conexión y uso de Serena en esta ejecución. |
147
+ | `klap init --no-graphify [ruta]` | Omite instalación y ejecución de Graphify en esta ejecución. |
148
+ | `klap update [ruta]` | Reutiliza proveedores compatibles y actualiza el contexto. |
146
149
  | `klap status [ruta]` | Indica si el contexto está actualizado respecto a Git. |
147
150
  | `klap open [ruta]` | Abre el portal humano estático. |
148
151
  | `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
149
152
  | `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
150
153
  | `klap doctor --agent [ruta]` | Comprueba AGENTS.md, freshness, Graphify y configuración MCP. |
154
+ | `klap doctor --agent --repair [ruta]` | Repara proveedores estándar en runtimes locales. |
151
155
  | `klap scope [ruta]` | Explica archivos incluidos/excluidos y sus motivos. |
152
156
  | `klap migrate-graph [ruta]` | Planea la migración de grafos legacy; `--apply` preserva respaldos. |
153
157
  | `klap --version` | Muestra la versión instalada. |
@@ -4,7 +4,7 @@
4
4
 
5
5
  KlapContext convierte evidencia de código en un modelo del sistema: puntos de
6
6
  entrada, flujos, automatizaciones, integraciones y sus incertidumbres.
7
- Graphify puede funcionar como proveedor opcional de análisis técnico;
7
+ Graphify puede funcionar como proveedor estándar de análisis técnico;
8
8
  Serena puede sumar navegación semántica on demand por MCP; KlapContext conserva
9
9
  analizadores locales y transforma toda evidencia mediante contratos
10
10
  desacoplados para personas y agentes de IA.
@@ -32,12 +32,11 @@ Repositorio → KlapContext → Contexto de ingeniería
32
32
 
33
33
  ```bash
34
34
  pip install klapcontext
35
- # opcional, para sumar el grafo Graphify
36
- pip install "klapcontext[graphify]"
37
35
  ```
38
36
 
39
- La instalación base no requiere Graphify. Como alternativa para una CLI
40
- aislada: `pipx install klapcontext` o `pipx install "klapcontext[graphify]"`.
37
+ La instalación base no incorpora proveedores en el intérprete de KlapContext.
38
+ El primer `klap init` los prepara en entornos aislados bajo
39
+ `.klap/provider-runtime/`.
41
40
 
42
41
  ## Primer uso
43
42
 
@@ -52,22 +51,24 @@ bajo `.klap/` y prepara un `AGENTS.md` privado para que los agentes descubran el
52
51
  contexto progresivamente. Ambos se agregan a `.git/info/exclude`; KlapContext
53
52
  nunca modifica tu `.gitignore`.
54
53
 
55
- Graphify se descubre automáticamente por su ejecutable o módulo Python: no hace
56
- falta habilitarlo. Si está disponible, `init` ejecuta una extracción real y usa
54
+ `klap init` prepara Graphify y Serena con versiones fijadas si no encuentra una
55
+ instalación compatible. Si una preparación falla, los analizadores nativos
56
+ siguen generando contexto y el resumen registra el motivo. Graphify ejecuta una extracción real y usa
57
57
  `.klap/graphify-out/graph.json` en `context.json`, el portal y la configuración
58
58
  MCP. Si no está instalado o falla, los analizadores nativos siguen generando un
59
59
  contexto útil y el reporte queda `PARTIAL`, con proveedores detectados,
60
60
  ejecutados y omitidos (incluido el motivo). `context.json.analysis` conserva la
61
- cobertura, trazas y contribuciones combinadas con procedencia. KlapContext no
62
- instala proveedores
63
- durante `init`. Se puede deshabilitar Graphify explícitamente con
61
+ cobertura, trazas y contribuciones combinadas con procedencia. Se puede
62
+ deshabilitar Graphify explícitamente con
64
63
  `.klap/providers.json` y `{"providers":{"graphify":{"enabled":false}}}`.
65
64
 
66
65
  [Serena](https://github.com/oraios/serena) se integra de forma independiente y
67
- opcional. Si `serena` está instalado o configurado, KlapContext descubre sus
66
+ intercambiable. Si falta una instalación compatible, Serena 1.7.0 se prepara
67
+ en el runtime local; KlapContext descubre sus
68
68
  tools reales por MCP y ofrece símbolos, definiciones, referencias,
69
- implementaciones e impacto por consulta. No ejecuta esas búsquedas durante
70
- `klap init`, no instala Serena y nunca llama sus tools de escritura, shell,
69
+ implementaciones e impacto por consulta. Durante `klap init` sólo ejecuta una
70
+ comprobación acotada de overview sobre un archivo elegible para verificar MCP;
71
+ no realiza búsquedas semánticas amplias, y nunca llama sus tools de escritura, shell,
71
72
  onboarding o memorias. Ver [instalación, configuración, seguridad,
72
73
  compatibilidad y límites](docs/SERENA.md).
73
74
 
@@ -105,12 +106,15 @@ klap scope --json
105
106
  | Comando | Descripción |
106
107
  | --- | --- |
107
108
  | `klap init [ruta]` | Genera el contexto inicial. |
108
- | `klap update [ruta]` | Actualiza los proveedores disponibles y el contexto. |
109
+ | `klap init --no-serena [ruta]` | Omite instalación, conexión y uso de Serena en esta ejecución. |
110
+ | `klap init --no-graphify [ruta]` | Omite instalación y ejecución de Graphify en esta ejecución. |
111
+ | `klap update [ruta]` | Reutiliza proveedores compatibles y actualiza el contexto. |
109
112
  | `klap status [ruta]` | Indica si el contexto está actualizado respecto a Git. |
110
113
  | `klap open [ruta]` | Abre el portal humano estático. |
111
114
  | `klap agent [ruta]` | Muestra la configuración MCP local de Graphify. |
112
115
  | `klap context "tarea" [ruta]` | Compila un contexto focalizado reutilizando `.klap/context.json` si existe. |
113
116
  | `klap doctor --agent [ruta]` | Comprueba AGENTS.md, freshness, Graphify y configuración MCP. |
117
+ | `klap doctor --agent --repair [ruta]` | Repara proveedores estándar en runtimes locales. |
114
118
  | `klap scope [ruta]` | Explica archivos incluidos/excluidos y sus motivos. |
115
119
  | `klap migrate-graph [ruta]` | Planea la migración de grafos legacy; `--apply` preserva respaldos. |
116
120
  | `klap --version` | Muestra la versión instalada. |
@@ -1,5 +1,5 @@
1
1
  """KlapContext local engineering-context generator."""
2
2
 
3
- __version__ = "0.8.2"
3
+ __version__ = "0.9.0"
4
4
 
5
5
 
@@ -10,6 +10,7 @@ from .git import commit, exclude_klap, is_repository
10
10
  from .freshness import evaluate_freshness
11
11
  from .portal import write as write_portal
12
12
  from .providers import AnalysisServices, ResultState, migrate_legacy_graphs
13
+ from .providers.lifecycle import prepare_standard_providers, write_provider_state
13
14
  from .scope import IndexScope
14
15
 
15
16
 
@@ -17,20 +18,36 @@ def root_path(value: str | None) -> Path:
17
18
  return Path(value or os.getcwd()).resolve()
18
19
 
19
20
 
20
- def generate_all(root: Path, update: bool = False, *, shared: bool = False, allow_tracked_agents: bool = False, services: AnalysisServices | None = None) -> dict:
21
+ def generate_all(root: Path, update: bool = False, *, shared: bool = False, allow_tracked_agents: bool = False, services: AnalysisServices | None = None, no_graphify: bool = False, no_serena: bool = False) -> dict:
21
22
  if not is_repository(root):
22
23
  raise RuntimeError(f"Not a Git repository: {root}")
23
24
  exclude_klap(root)
24
25
  klap = root / ".klap"
25
26
  klap.mkdir(exist_ok=True)
26
27
  scope = IndexScope.load(root)
27
- services = services or AnalysisServices(root=root)
28
- context, graph_result = services.build_context(root, scope=scope, update=update)
28
+ lifecycle = {} if services is not None else prepare_standard_providers(root, no_graphify=no_graphify, no_serena=no_serena)
29
+ services = services or AnalysisServices(root=root, graphify=not no_graphify, serena=not no_serena)
30
+ try:
31
+ context, graph_result = services.build_context(root, scope=scope, update=update)
32
+ finally:
33
+ services.close()
29
34
  files = next((item.get("files", []) for item in graph_result.trace if item.get("files") is not None), [])
35
+ graphify_ready = graph_result.state == ResultState.OK and graph_result.provider is not None and graph_result.provider.name == "graphify"
36
+ if lifecycle and "graphify" in lifecycle and lifecycle["graphify"].get("state") == "READY":
37
+ graph_runtime = lifecycle["graphify"]
38
+ if graphify_ready:
39
+ graph_runtime["connection"] = "VERIFIED"
40
+ else:
41
+ graph_runtime.update({
42
+ "state": "PARTIAL",
43
+ "connection": "FAILED",
44
+ "error": graph_result.error or "Graphify extraction did not produce a valid canonical graph",
45
+ })
46
+ write_provider_state(root, lifecycle)
30
47
  context["freshness"] = {"status": "CURRENT", "reason": "Generation completed from the recorded scope snapshot."}
48
+ context["analysis"]["provider_lifecycle"] = lifecycle
31
49
  (klap / "context.json").write_text(json.dumps(context, indent=2) + "\n", encoding="utf-8")
32
50
  agent = write_agent(context, klap / "agent-context.md")
33
- graphify_ready = graph_result.state == ResultState.OK and graph_result.provider is not None and graph_result.provider.name == "graphify"
34
51
  mcp = generate_mcp_configs(root) if graphify_ready else {"status": "unavailable", "reason": "Graphify graph unavailable; native context remains usable."}
35
52
  setup = AgentInstructionsManager(root).apply(context, shared=shared, allow_tracked=allow_tracked_agents)
36
53
  context["agent_onboarding"] = {**setup.as_dict(), "mcp": mcp}
@@ -42,6 +59,7 @@ def generate_all(root: Path, update: bool = False, *, shared: bool = False, allo
42
59
  "git_commit": commit(root),
43
60
  "provider": {"name": graph_result.provider.name if graph_result.provider else None, "version": graph_result.provider.version if graph_result.provider else None, "state": graph_result.state.value, "warnings": graph_result.warnings, "trace": graph_result.trace},
44
61
  "analysis": context["analysis"],
62
+ "provider_lifecycle": lifecycle,
45
63
  "klap_version": __version__,
46
64
  "agent_onboarding": setup.as_dict(),
47
65
  "scope": {"rules_version": context["system_model"]["index_scope"]["rules_version"], "hash": scope.snapshot_hash(), "configuration": ".klap/scope.json"},
@@ -63,10 +81,13 @@ def cmd_init(args):
63
81
  print("KlapContext\n")
64
82
  shared, allow = _permission(args, root, "Agregar o actualizar")
65
83
  try:
66
- context = generate_all(root, shared=shared, allow_tracked_agents=allow) if shared or allow else generate_all(root)
84
+ no_graphify, no_serena = getattr(args, "no_graphify", False), getattr(args, "no_serena", False)
85
+ context = generate_all(root, shared=shared, allow_tracked_agents=allow, no_graphify=no_graphify, no_serena=no_serena) if shared or allow or no_graphify or no_serena else generate_all(root)
67
86
  except RuntimeError as error:
68
87
  print(f"✗ {error}", file=sys.stderr)
69
88
  return 1
89
+ for name, details in context.get("analysis", {}).get("provider_lifecycle", {}).items():
90
+ print(f"Provider {name}: {details.get('state')} · {details.get('connection')}" + (f" ({details['error']})" if details.get("error") else ""))
70
91
  graph_provider = context.get("providers", {}).get("dependency_graph", {})
71
92
  graph_line = "✓ Dependency graph generated" if graph_provider.get("state") == "OK" else "! External graph unavailable; native analyzers used"
72
93
  print("✓ Git repository detected\n" + graph_line + "\n✓ Engineering context generated\n✓ Agent context generated\n✓ Human portal generated")
@@ -100,7 +121,8 @@ def cmd_update(args):
100
121
  root = root_path(args.path)
101
122
  shared, allow = _permission(args, root, "Actualizar")
102
123
  try:
103
- context = generate_all(root, update=True, shared=shared, allow_tracked_agents=allow) if shared or allow else generate_all(root, update=True)
124
+ no_graphify, no_serena = getattr(args, "no_graphify", False), getattr(args, "no_serena", False)
125
+ context = generate_all(root, update=True, shared=shared, allow_tracked_agents=allow, no_graphify=no_graphify, no_serena=no_serena) if shared or allow or no_graphify or no_serena else generate_all(root, update=True)
104
126
  except RuntimeError as error:
105
127
  print(f"✗ {error}", file=sys.stderr)
106
128
  return 1
@@ -120,8 +142,12 @@ def cmd_status(args):
120
142
 
121
143
 
122
144
  def cmd_doctor(args):
123
- result = doctor_agent(root_path(args.path), probe_mcp=getattr(args, "probe_mcp", False))
145
+ root = root_path(args.path)
146
+ repaired = prepare_standard_providers(root, repair=True) if getattr(args, "repair", False) else {}
147
+ result = doctor_agent(root, probe_mcp=getattr(args, "probe_mcp", False))
124
148
  print("KlapContext — Agent Doctor\n")
149
+ for name, details in repaired.items():
150
+ print(f"Repair {name}: {details.get('state')} · {details.get('connection')}" + (f" ({details['error']})" if details.get("error") else ""))
125
151
  checks = [
126
152
  (result["agents"]["valid"], "AGENTS.md configured"),
127
153
  (result["context"]["available"], "Engineering context available"),
@@ -225,11 +251,14 @@ def main(argv=None):
225
251
  if name in {"init", "update"}:
226
252
  command.add_argument("--shared", action="store_true", help="Create versionable AGENTS.md instructions")
227
253
  command.add_argument("--allow-tracked-agents", action="store_true", help="Authorize updating a tracked AGENTS.md")
254
+ command.add_argument("--no-serena", action="store_true", help="Do not install, connect or use Serena for this run")
255
+ command.add_argument("--no-graphify", action="store_true", help="Do not install, run or use Graphify for this run")
228
256
  command.set_defaults(func=func)
229
257
  command = subs.add_parser("doctor", help="Diagnose project and agent readiness")
230
258
  command.add_argument("path", nargs="?", default=".")
231
259
  command.add_argument("--agent", action="store_true", help="Check AGENTS.md and generated context")
232
260
  command.add_argument("--probe-mcp", action="store_true", help="Start Graphify MCP briefly and stop it")
261
+ command.add_argument("--repair", action="store_true", help="Install or repair standard providers in isolated project runtimes")
233
262
  command.set_defaults(func=cmd_doctor)
234
263
  command = subs.add_parser("migrate-graph", help="Safely plan or apply legacy graph storage migration")
235
264
  command.add_argument("path", nargs="?", default=".")
@@ -55,6 +55,9 @@ def exclude_klap(root: Path) -> None:
55
55
  exclude = (root / git_dir / "info" / "exclude").resolve()
56
56
  exclude.parent.mkdir(parents=True, exist_ok=True)
57
57
  existing = exclude.read_text(encoding="utf-8") if exclude.exists() else ""
58
- additions = [entry for entry in (".klap/", "graphify-out/") if entry not in {line.strip() for line in existing.splitlines()}]
58
+ # Serena manages project-local language-server metadata in `.serena/`.
59
+ # Keep it private like `.klap/` without modifying the repository's shared
60
+ # .gitignore or hiding an explicitly tracked file.
61
+ additions = [entry for entry in (".klap/", "graphify-out/", ".serena/") if entry not in {line.strip() for line in existing.splitlines()}]
59
62
  if additions:
60
63
  exclude.write_text(existing.rstrip() + ("\n" if existing.strip() else "") + "\n".join(additions) + "\n", encoding="utf-8")
@@ -42,14 +42,19 @@ def _scoped_graphifyignore(root: Path, scope: IndexScope):
42
42
  path.unlink(missing_ok=True)
43
43
 
44
44
 
45
- def executable() -> str | None:
45
+ def executable(root: Path | None = None) -> str | None:
46
+ if root is not None:
47
+ from .providers.lifecycle import managed_command
48
+ managed = managed_command(Path(root), "graphify")
49
+ if managed:
50
+ return managed
46
51
  return shutil.which("graphify")
47
52
 
48
53
 
49
- def command_prefix() -> list[str] | None:
54
+ def command_prefix(root: Path | None = None) -> list[str] | None:
50
55
  """Use the console script when present; PyPI also supports python -m graphify."""
51
- if executable():
52
- return [executable()]
56
+ if executable(root):
57
+ return [executable(root)]
53
58
  try:
54
59
  import graphify # noqa: F401
55
60
  return [sys.executable, "-m", "graphify"]
@@ -57,7 +62,16 @@ def command_prefix() -> list[str] | None:
57
62
  return None
58
63
 
59
64
 
60
- def version() -> str | None:
65
+ def version(root: Path | None = None) -> str | None:
66
+ if root is not None:
67
+ command = executable(root)
68
+ if command:
69
+ try:
70
+ output = subprocess.run([command, "--version"], text=True, capture_output=True, timeout=5, check=False)
71
+ if output.returncode == 0:
72
+ return (output.stdout or output.stderr).strip().rsplit(" ", 1)[-1]
73
+ except (OSError, subprocess.SubprocessError):
74
+ pass
61
75
  try:
62
76
  return importlib.metadata.version("graphifyy")
63
77
  except importlib.metadata.PackageNotFoundError:
@@ -117,7 +131,11 @@ def migrate_legacy_graphs(root: Path, *, apply: bool = False) -> dict:
117
131
 
118
132
 
119
133
  def generate(root: Path, update: bool = False, scope: IndexScope | None = None) -> Path:
120
- prefix = command_prefix()
134
+ # Preserve the public no-argument command resolver for existing callers;
135
+ # only a project-managed runtime needs an explicit root-aware prefix.
136
+ from .providers.lifecycle import managed_command
137
+ managed = managed_command(root, "graphify")
138
+ prefix = [managed] if managed else command_prefix()
121
139
  if not prefix:
122
140
  raise GraphifyError("Graphify is required. Install it with: pipx install graphifyy")
123
141
  scope = scope or IndexScope.load(root)
@@ -0,0 +1,109 @@
1
+ """Offline, evidence-backed HTML portal."""
2
+ from __future__ import annotations
3
+
4
+ import html
5
+ import json
6
+ from pathlib import Path
7
+
8
+
9
+ def e(value: object) -> str:
10
+ return html.escape(str(value))
11
+
12
+
13
+ def empty(message: str) -> str:
14
+ return f"<p class='empty'>{e(message)}</p>"
15
+
16
+
17
+ def items(rows: list[dict], key: str = "name") -> str:
18
+ if not rows:
19
+ return empty("Sin evidencia suficiente todavía.")
20
+ return "".join(
21
+ f"<li><i></i><div><b>{e(row.get(key) or row.get('name') or row.get('path') or row.get('intent') or 'Sin nombre')}</b><small>{e(row.get('type') or row.get('role') or row.get('reason') or row.get('description') or '')}</small></div></li>"
22
+ for row in rows
23
+ )
24
+
25
+
26
+ def map_html(interactions: dict) -> str:
27
+ nodes = interactions.get("nodes", [])[:8]
28
+ if not nodes:
29
+ return "<p class='map-empty'>El mapa aparecerá cuando haya interacciones verificables.</p>"
30
+ positions = [(75, 75), (220, 42), (220, 130), (220, 214), (400, 75), (400, 174), (565, 130), (75, 208)]
31
+ names = [node.get("name", f"node-{index}") for index, node in enumerate(nodes)]
32
+ points = dict(zip(names, positions))
33
+ edges = "".join(
34
+ f"<path d='M {points[edge['source']][0]} {points[edge['source']][1]} C {points[edge['source']][0]+55} {points[edge['source']][1]}, {points[edge['target']][0]-55} {points[edge['target']][1]}, {points[edge['target']][0]} {points[edge['target']][1]}'/>"
35
+ for edge in interactions.get("edges", [])[:14]
36
+ if edge.get("source") in points and edge.get("target") in points
37
+ )
38
+ boxes = "".join(
39
+ f"<g class='node' tabindex='0' role='link' data-evidence-link><rect x='{points[name][0]-50}' y='{points[name][1]-18}' width='100' height='36' rx='6'/><text x='{points[name][0]}' y='{points[name][1]-1}'>{e(node.get('name', 'componente'))[:14]}</text><text class='kind' x='{points[name][0]}' y='{points[name][1]+11}'>{e(node.get('type', 'componente'))[:14]}</text></g>"
40
+ for name, node in zip(names, nodes)
41
+ )
42
+ return f"<div class='map'><svg viewBox='0 0 640 260' role='img' aria-label='Mapa de relaciones; cada componente lleva a las evidencias'><g class='edges'>{edges}</g>{boxes}</svg></div>"
43
+
44
+
45
+ def view(title: str, name: str, body: str, icon: str) -> str:
46
+ return f"<section class='view' id='{name}' aria-labelledby='{name}-title'><header class='view-head'><span aria-hidden='true'>{icon}</span><div><h2 id='{name}-title'>{e(title)}</h2><p>Datos extraídos del repositorio y respaldados por evidencia.</p></div></header>{body}</section>"
47
+
48
+
49
+ def tree(files: list[dict]) -> str:
50
+ if not files:
51
+ return empty("No se encontraron archivos.")
52
+ rows = []
53
+ for file in files:
54
+ symbols = "".join(f"<li>{e(x.get('name', 'símbolo'))} <small>{e(x.get('kind', ''))}</small></li>" for x in file.get("symbols", [])[:100])
55
+ rows.append(f"<details class='source-file'><summary>{e(file.get('path', 'archivo'))} <small>{len(file.get('symbols', []))} símbolos</small></summary><ul>{symbols or '<li>Sin símbolos detectados.</li>'}</ul></details>")
56
+ return "".join(rows)
57
+
58
+
59
+ CSS = """
60
+ :root{--bg:#101820;--surface:#1B2630;--up:#263440;--cyan:#00E5F0;--cyan2:#40F5FF;--ink:#F3F6F8;--muted:#AAB9C5;--line:#354650;--ok:#39DFA6;--warn:#FFBF69;--bad:#FF657A}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:320px;color:var(--ink);font:15px/1.5 Inter,ui-sans-serif,system-ui,sans-serif;background:var(--bg);background-image:linear-gradient(#fff2 1px,transparent 1px),linear-gradient(90deg,#fff2 1px,transparent 1px);background-size:28px 28px}button,input,select{font:inherit}button,a,summary{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:3px solid var(--cyan);outline-offset:3px}.portal{max-width:1400px;margin:auto;padding:16px}.frame{overflow:hidden;border:1px solid var(--line);border-radius:17px;background:#111c24e8;box-shadow:0 22px 65px #0009}.mast{display:flex;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid var(--line)}.mark{display:grid;place-items:center;width:35px;height:35px;border-radius:9px;background:#d4dde1;color:#101820;font-size:22px;font-weight:900}.mast b{font-size:14px}.mast small,.view-head p,small,.empty{display:block;color:var(--muted);font-size:11px}.state{margin-left:auto;color:var(--ok);font:11px ui-monospace,monospace}.nav{position:sticky;top:0;z-index:10;display:flex;gap:7px;overflow-x:auto;padding:12px 20px;border-bottom:1px solid var(--line);background:#111c24f5}.nav a{flex:none;border:1px solid var(--line);border-radius:7px;padding:6px 11px;background:#14212a;color:var(--ink);font-size:11px;font-weight:800;text-decoration:none}.nav a:hover,.nav a.active{border-color:var(--cyan);background:var(--cyan);color:#071217}.hero{display:grid;grid-template-columns:minmax(0,1fr) minmax(370px,.95fr);gap:28px;padding:28px;border-bottom:1px solid var(--line)}.eyebrow{color:var(--cyan);font:700 10px ui-monospace,monospace;letter-spacing:.16em}.identity h1{margin:7px 0 10px;font-family:Impact,Haettenschweiler,'Arial Narrow Bold',sans-serif;font-size:clamp(42px,7vw,76px);line-height:.85;letter-spacing:.04em;overflow-wrap:anywhere;text-shadow:3px 3px var(--cyan)}.identity p{max-width:450px;font-size:16px;font-weight:650}.tags{display:flex;flex-wrap:wrap;gap:7px}.tags span{padding:3px 8px;border:1px solid var(--cyan);border-radius:5px;font:10px ui-monospace,monospace}.copy{margin-top:18px;border:0;border-radius:7px;padding:12px 17px;background:var(--cyan);color:#071217;font-weight:950;letter-spacing:.04em;box-shadow:0 4px #008c96}.copy:hover{background:var(--cyan2)}.intro{color:var(--muted);font-size:12px!important;font-weight:400!important}.checks{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin:18px 0 0;padding:14px 0 0;border-top:2px solid var(--ink)}.checks li{list-style:none;color:var(--muted);font-size:10px}.console-wrap{position:relative;padding-top:25px}.label{position:absolute;z-index:2;top:0;left:50%;transform:translateX(-50%);min-width:180px;border:4px solid #38505c;border-radius:8px 8px 0 0;padding:5px 10px;background:var(--cyan);color:#061519;text-align:center;font:900 10px ui-monospace,monospace}.console{border:4px solid #596872;border-radius:25px;padding:21px 22px 28px;background:linear-gradient(135deg,#80909a,#3d4a52 55%,#667680);box-shadow:inset 2px 2px #aeb9be,inset -4px -5px #263038,0 16px 30px #0008}.screen{min-height:310px;border:8px solid #121b20;border-radius:13px;padding:13px;background:#061d26;box-shadow:inset 0 0 0 2px #274552,inset 0 0 24px #00e5f033;color:var(--cyan)}.screen-top{display:flex;justify-content:space-between;gap:8px;border-bottom:1px solid var(--cyan);padding-bottom:6px;font:800 10px ui-monospace,monospace}.screen h2{font:900 14px ui-monospace,monospace}.map{min-height:130px;overflow:auto;border:1px dashed #00e5f099}.map svg{display:block;min-width:440px;width:100%;height:130px}.edges path{fill:none;stroke:#00e5f0aa;stroke-width:1.5}.node{cursor:pointer}.node rect{fill:#0b3542;stroke:var(--cyan)}.node text{fill:var(--ink);font:10px ui-monospace,monospace;text-anchor:middle}.node .kind{fill:#9eeff3;font-size:8px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-top:9px;padding-top:8px;border-top:1px solid #00e5f066}.stats b{display:block;font:900 19px ui-monospace,monospace}.stats span{font-size:8px;color:#b0eef2}.note{font:9px/1.35 ui-monospace,monospace}.controls{display:grid;grid-template-columns:1fr 72px;gap:14px;align-items:center;margin-top:15px}.mini{display:flex;gap:8px;justify-content:center}.mini button,.mini a{border:1px solid #1c3f49;border-radius:999px;padding:5px 9px;background:#17323c;color:var(--cyan);font:900 8px ui-monospace,monospace;text-decoration:none}.buttons{display:flex;justify-content:space-between}.buttons button,.buttons a{display:grid;place-items:center;width:32px;height:32px;border:2px solid #087b84;border-radius:50%;background:var(--cyan);color:#071217;font-weight:950;box-shadow:0 3px #075960;text-decoration:none}.view{scroll-margin-top:60px;padding:32px 28px;border-bottom:1px solid var(--line)}.view-head{display:flex;gap:9px;align-items:center;margin-bottom:16px}.view-head>span{display:grid;place-items:center;width:31px;height:31px;border:1px solid var(--cyan);border-radius:6px;color:var(--cyan)}.view-head h2{margin:0;font-size:20px}.view-head p{margin:1px 0}.grid,.two,.columns{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.card,.flow,.trace,.tech{padding:16px;border:1px solid var(--line);border-radius:9px;background:var(--surface)}.list{margin:0;padding:0;list-style:none}.list li{display:flex;gap:8px;padding:9px 0;border-bottom:1px solid var(--line)}.list li:last-child{border:0}.list i{flex:none;width:7px;height:7px;margin-top:7px;border-radius:50%;background:var(--cyan)}.evidence mark{margin-left:auto;height:max-content;background:#16453a;color:var(--ok);font-size:9px}.unknown{margin-top:12px}.links{display:flex;flex-wrap:wrap;gap:9px;margin:12px 0}.links a{flex:1;border:1px solid var(--cyan);border-radius:6px;padding:9px;background:#122b35;color:var(--cyan);font-weight:800;text-decoration:none}.files{max-height:70vh;overflow:auto;margin-top:12px;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--surface)}.files details{padding:5px;border-bottom:1px solid var(--line)}.files summary{cursor:pointer}.files ul{padding-left:20px}#search,select{width:100%;padding:10px;border:1px solid var(--line);border-radius:6px;background:var(--surface);color:var(--ink)}#trace{margin-top:10px;color:var(--cyan);font-family:ui-monospace,monospace}.help{max-width:440px;border:1px solid var(--cyan);border-radius:10px;background:var(--up);color:var(--ink);box-shadow:0 20px 60px #000b}.help::backdrop{background:#000b}.help button{float:right;border:0;border-radius:5px;padding:6px 9px;background:var(--cyan);color:#071217;font-weight:800}@media(max-width:900px){.hero{grid-template-columns:1fr;max-width:680px;margin:auto}.console-wrap{order:-1}}@media(max-width:620px){.portal{padding:0}.frame{border-radius:0;border-left:0;border-right:0}.mast{padding:10px 13px}.mast small{display:none}.nav{padding:9px 12px}.hero,.view{padding:20px 14px}.identity h1{font-size:51px}.console{padding:17px 13px 24px}.screen{padding:10px}.grid,.two,.columns,.checks{grid-template-columns:1fr}}
61
+ """
62
+
63
+
64
+ def render(context: dict, agent_text: str, graphify_files: list[str]) -> str:
65
+ project, system = context["project"], context.get("system_model", {})
66
+ stack = [item for values in context.get("stack", {}).values() if isinstance(values, list) for item in values]
67
+ points, flows, evidence = system.get("entry_points", []), system.get("main_flows", []), context.get("evidence", [])
68
+ unknowns, scope, explore = system.get("unknowns", []), system.get("index_scope", {}), system.get("exploration", {})
69
+ framework = system.get("framework") or {}
70
+ state = context.get("freshness", {}).get("status") or ("CURRENT" if project.get("git_commit") else "UNINITIALIZED")
71
+ project_name = project.get("name") or "Proyecto sin nombre"
72
+ tags = "".join(f"<span>{e(x)}</span>" for x in stack[:6]) or "<span>Stack no identificado</span>"
73
+ if framework.get("version"):
74
+ tags += f"<span>{e(framework.get('name', 'Framework'))} {e(framework['version'])}</span>"
75
+ if framework.get("php_version"):
76
+ tags += f"<span>PHP {e(framework['php_version'])}</span>"
77
+ purpose = system.get("purpose", {}).get("text") or "No pudimos determinar el propósito con suficiente confianza."
78
+ flow_cards = "".join(f"<article class='flow'><b>{e(flow.get('name', 'Flujo detectado'))}</b><p>{' <em>→</em> '.join(e(step.get('name', 'paso')) for step in flow.get('steps', []))}</p></article>" for flow in flows) or empty("No se detectaron flujos principales con confianza.")
79
+ evidence_rows = "".join(f"<li><i></i><div><b>{e(x.get('path', 'archivo'))}</b><small>{e(x.get('reason', 'Evidencia detectada'))}</small></div><mark>{e(x.get('status', 'CONFIRMED'))}</mark></li>" for x in evidence) or empty("No se registró evidencia aún.")
80
+ options = "".join(f"<option value='{i}'>{e(x.get('name', 'Flujo'))}</option>" for i, x in enumerate(flows))
81
+ traces = json.dumps([[step.get("name", "paso") for step in x.get("steps", [])] for x in flows]).replace("</", "<\\/")
82
+ graph_links = "".join(f"<a href='graphify-out/{name}' target='_blank' rel='noreferrer'>{label} ↗</a>" for name, label in [("graph.html", "Abrir grafo técnico"), ("GRAPH_REPORT.md", "Abrir informe técnico")] if name in graphify_files) or empty("No hay artefactos de Graphify disponibles.")
83
+ providers = [{"name": x.get("name"), "description": f"{x.get('status', 'UNKNOWN')} · {'ejecutado' if x.get('executed') else x.get('reason', 'no ejecutado')}"} for x in context.get("analysis", {}).get("providers", [])]
84
+ roots = [{"name": x.get("path"), "description": f"{x.get('files', 0)} archivos incluidos"} for x in scope.get("included_roots", [])]
85
+ excluded = [{"name": x, "description": f"{n} archivos"} for x, n in scope.get("excluded_reasons", {}).items()]
86
+ call_cards = "".join(f"<article class='flow'><b>{e(x.get('name', 'Recorrido'))}</b><p>{' <em>→</em> '.join(e(s.get('name', 'paso')) for s in x.get('steps', []))}</p></article>" for x in explore.get("call_paths", [])) or empty("No hay recorridos de llamadas verificables en el grafo.")
87
+ recent = "".join(f"<li><i></i><div><b>{e(x.get('subject', 'Cambio'))}</b><small>{e(x.get('date', ''))} · {e(x.get('commit', '')[:10])}</small></div></li>" for x in system.get("recent_changes", [])) or empty("No hay historial disponible.")
88
+ views = [
89
+ view("Sistema", "sistema", f"<div class='tech'><h3>Resumen de {e(project_name)}</h3><p>{e(system.get('project_story', {}).get('text') or purpose)}</p></div>", "⌘"),
90
+ view("Flujos principales", "flujos", f"<div class='grid'>{flow_cards}</div>" + (f"<div class='trace'><label for='flow-select'>TRAZAR UN FLUJO</label><select id='flow-select'>{options}</select><div id='trace'></div></div>" if flows else ""), "⌁"),
91
+ view("Entradas", "entradas", f"<div class='two'><article class='card'><h3>Puntos de entrada</h3><ul class='list'>{items(points, 'intent')}</ul></article><article class='card'><h3>Entradas y salidas</h3><div class='columns'><div><label>ENTRADAS</label><ul class='list'>{items(system.get('inputs', []), 'description')}</ul></div><div><label>SALIDAS</label><ul class='list'>{items(system.get('outputs', []), 'description')}</ul></div></div></article></div>", "⇢"),
92
+ view("Evidencias", "evidencia", f"<article class='card'><h3>Hechos que sostienen el mapa</h3><ul class='list evidence'>{evidence_rows}</ul></article><article class='card unknown'><h3>Lo que todavía no sabemos</h3><ul class='list'>{items([{'name': x, 'type': 'requiere validación'} for x in unknowns])}</ul></article>", "◈"),
93
+ view("Index Scope", "scope", f"<div class='tech'><h3>Qué entró al índice</h3><p>{scope.get('included_code_files', 0)} archivos de código incluidos; {scope.get('excluded_files', 0)} recursos excluidos; {scope.get('graph_nodes_outside_scope', 0)} nodos fuera del alcance. Configuración: <code>.klap/scope.json</code>.</p></div><div class='two'><article class='card'><h3>Directorios incluidos</h3><ul class='list'>{items(roots)}</ul></article><article class='card'><h3>Exclusiones por motivo</h3><ul class='list'>{items(excluded)}</ul></article></div>", "◎"),
94
+ view("Archivos y símbolos", "archivos", f"<input id='search' type='search' placeholder='Buscar archivo o símbolo' aria-label='Buscar archivo o símbolo'><div class='files'>{tree(explore.get('files', []))}</div>", "▤"),
95
+ view("Recorridos del grafo", "recorridos", f"<p class='empty'>Estas rutas muestran llamadas conectadas en Graphify; no implican todas las ramas.</p><div class='grid'>{call_cards}</div>", "⌁"),
96
+ view("Historial Git", "historial", f"<article class='card'><h3>Cambios recientes</h3><ul class='list'>{recent}</ul></article>", "◷"),
97
+ view("Graphify y proveedores", "tecnico", f"<div class='tech'><h3>Profundizá en el análisis del código</h3><p>{e(system.get('project_story', {}).get('text') or 'Todavía no hay una narrativa técnica verificable.')}</p></div><div class='links'>{graph_links}</div><div class='two'><article class='card'><h3>Dependencias</h3><ul class='list'>{items(system.get('dependencies', []), 'package')}</ul></article><article class='card'><h3>Proveedores precompilados</h3><ul class='list'>{items(providers)}</ul></article></div>", "⌬"),
98
+ ]
99
+ onboarding = context.get("agent_onboarding", {})
100
+ checks = "".join(["<li>Reporte generado desde evidencia local</li>", f"<li>{'✓' if onboarding.get('status') in {'configured', 'unchanged'} else '○'} AGENTS.md</li>", "<li>✓ Contexto en .klap/</li>", f"<li>{'✓' if graphify_files else '○'} Graphify</li>", f"<li>{'✓' if onboarding.get('mcp', {}).get('available', False) else '○'} MCP</li>"])
101
+ console = f"<div class='console-wrap'><div class='label'>ANÁLISIS LOCAL<br><small>{len(evidence)} evidencias</small></div><section class='console' aria-label='Consola de análisis de {e(project_name)}'><div class='screen'><div class='screen-top'><span>{e(project_name)}</span><span>{e(state)}</span></div><h2>Mapa del sistema</h2>{map_html(system.get('system_interactions', {}))}<div class='stats'><div><span>Entradas</span><b>{len(points)}</b></div><div><span>Flujos</span><b>{len(flows)}</b></div><div><span>Confirmados</span><b>{len(evidence)}</b></div><div><span>Pendientes</span><b>{len(unknowns)}</b></div></div><p class='note'>Abrí una sección para profundizar en el análisis y su evidencia.</p></div><div class='controls'><div class='mini'><button data-help>Select · ayuda</button><a href='#sistema'>Start · explorar</a></div><div class='buttons'><button data-copy aria-label='Copiar contexto de agente'>A</button><a href='#evidencia' aria-label='Ir a evidencias'>B</a></div></div></section></div>"
102
+ script = """<script>const ctx=__CTX__,paths=__PATHS__,help=document.querySelector('#help'),links=[...document.querySelectorAll('.nav a')],sections=[...document.querySelectorAll('.view')];document.querySelectorAll('[data-copy]').forEach(b=>b.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(ctx);const t=b.textContent;b.textContent='✓';setTimeout(()=>b.textContent=t,1500)}catch{b.textContent='!'}}));document.querySelectorAll('[data-help]').forEach(b=>b.addEventListener('click',()=>help.showModal()));document.querySelector('[data-close]').addEventListener('click',()=>help.close());document.querySelectorAll('[data-evidence-link]').forEach(n=>{const open=()=>document.querySelector('#evidencia').scrollIntoView({behavior:'smooth'});n.addEventListener('click',open);n.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();open()}})});const q=document.querySelector('#search');if(q)q.addEventListener('input',()=>document.querySelectorAll('.source-file').forEach(f=>f.hidden=!f.textContent.toLowerCase().includes(q.value.toLowerCase())));const s=document.querySelector('#flow-select');if(s){const out=document.querySelector('#trace'),show=()=>out.textContent=(paths[s.value]||[]).join(' → ');s.addEventListener('change',show);show()}const observer=new IntersectionObserver(rows=>rows.forEach(row=>{if(row.isIntersecting)links.forEach(link=>link.classList.toggle('active',link.getAttribute('href')==='#'+row.target.id))}),{rootMargin:'-25% 0px -65% 0px'});sections.forEach(section=>observer.observe(section));</script>""".replace("__CTX__", json.dumps(agent_text).replace("</", "<\\/")).replace("__PATHS__", traces)
103
+ navigation = [("sistema", "Sistema"), ("flujos", "Flujos"), ("entradas", "Entradas"), ("evidencia", "Evidencias"), ("scope", "Index Scope"), ("archivos", "Archivos"), ("recorridos", "Recorridos"), ("historial", "Git"), ("tecnico", "Graphify")]
104
+ top = "<nav class='nav' aria-label='Navegación del reporte'>" + "".join(f"<a href='#{key}'>{label}</a>" for key, label in navigation) + "</nav>"
105
+ return f"<!doctype html><html lang='es'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>{e(project_name)} · KlapContext</title><style>{CSS}</style></head><body><div class='portal'><div class='frame'><header class='mast'><span class='mark'>K</span><div><b>KlapContext</b><small>Reporte de ingeniería del repositorio</small></div><span class='state'>● {e(state)}</span></header>{top}<section class='hero' aria-labelledby='project-title'><div class='identity'><span class='eyebrow'>REPORTE DE INGENIERÍA</span><h1 id='project-title'>{e(project_name)}</h1><p>{e(purpose)}</p><div class='tags'>{tags}</div><button class='copy' data-copy>COPIAR CONTEXTO →</button><p class='intro'>Briefing listo para usar con un agente y navegar la evidencia técnica del proyecto.</p><ul class='checks'>{checks}</ul></div>{console}</section>{''.join(views)}</div></div><dialog class='help' id='help'><div><button data-close>Cerrar</button><h2>Confianza y navegación</h2><p>Los hechos confirmados se extraen durante <code>klap init</code>. Cada sección conserva los datos precompilados y su trazabilidad; las ausencias se muestran explícitamente.</p></div></dialog>{script}</body></html>"
106
+
107
+
108
+ def write(context: dict, agent_text: str, graphify_files: list[str], output: Path) -> None:
109
+ output.write_text(render(context, agent_text, graphify_files), encoding="utf-8")
@@ -18,7 +18,7 @@ class GraphifyProvider:
18
18
  request = request or {}
19
19
  if request.get("existing_only"):
20
20
  return graphify.canonical_graph(Path(request.get("root", "."))).exists()
21
- return graphify.command_prefix() is not None
21
+ return graphify.command_prefix(Path(request.get("root", "."))) is not None
22
22
 
23
23
  def dependency_graph(self, request: Any) -> ProviderResult[dict]:
24
24
  request = request or {}