higpertext-cli 0.8.0__py3-none-any.whl

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 (335) hide show
  1. config/adapters_config.json +450 -0
  2. config/antigravity_agent_template.json +31 -0
  3. config/app_config.json +174 -0
  4. config/context_engine.json +33 -0
  5. config/environments/model_defaults.json +5 -0
  6. config/governance/branching_strategy.json +36 -0
  7. config/governance/deployment_gates.json +30 -0
  8. config/governance/guidelines_contract.json +54 -0
  9. config/governance/quality_gates.json +39 -0
  10. config/governance/section_rules.json +22 -0
  11. config/governance/security_guardrails.json +52 -0
  12. config/hooks/README.md +35 -0
  13. config/hooks/custom/test_output_limiter.json +9 -0
  14. config/hooks/global/session_prompt.json +9 -0
  15. config/htx_config.json +24 -0
  16. config/profile_learner.json +18 -0
  17. config/profiles/base_agent.json +40 -0
  18. config/profiles/base_auditor.json +19 -0
  19. config/profiles/base_developer.json +19 -0
  20. config/profiles/base_operator.json +16 -0
  21. config/profiles/global.json +33 -0
  22. config/profiles/software_developer.json +23 -0
  23. config/router_content.json +137 -0
  24. config/semantic_graph.json +66 -0
  25. config/workflows/ado_release_flow.json +38 -0
  26. config/workflows/docs-update.json +33 -0
  27. config/workflows/governance-check.yaml +26 -0
  28. config/workflows/guidelines-sync.json +40 -0
  29. config/workflows/higpertext-build.json +73 -0
  30. config/workflows/higpertext-plan.json +38 -0
  31. config/workflows/higpertext-review.json +41 -0
  32. config/workflows/pr-quality-check.json +56 -0
  33. config/workflows/quality-remediation.json +57 -0
  34. higpertext/__init__.py +18 -0
  35. higpertext/adapters/__init__.py +27 -0
  36. higpertext/adapters/adapter_utils.py +604 -0
  37. higpertext/adapters/claude_adapter/__init__.py +0 -0
  38. higpertext/adapters/claude_adapter/claude_adapter.py +154 -0
  39. higpertext/adapters/copilot_adapter/__init__.py +0 -0
  40. higpertext/adapters/copilot_adapter/copilot_adapter.py +231 -0
  41. higpertext/adapters/gemini_adapter/__init__.py +0 -0
  42. higpertext/adapters/gemini_adapter/gemini_adapter.py +211 -0
  43. higpertext/adapters/llm_formatter.py +46 -0
  44. higpertext/adapters/open_code_adapter/__init__.py +0 -0
  45. higpertext/adapters/open_code_adapter/open_code_adapter.py +480 -0
  46. higpertext/capabilities/capabilities_runner.py +216 -0
  47. higpertext/capabilities/common/agent-builder.json +54 -0
  48. higpertext/capabilities/common/agent-sync.json +34 -0
  49. higpertext/capabilities/common/code-skeletonizer.json +35 -0
  50. higpertext/capabilities/common/commit-report.json +42 -0
  51. higpertext/capabilities/common/context-assembler.json +37 -0
  52. higpertext/capabilities/common/context-budget-report.json +15 -0
  53. higpertext/capabilities/common/dep-manager.json +43 -0
  54. higpertext/capabilities/common/docs-sync.json +14 -0
  55. higpertext/capabilities/common/doctor.json +18 -0
  56. higpertext/capabilities/common/efficiency-meter.json +31 -0
  57. higpertext/capabilities/common/env-catalog.json +13 -0
  58. higpertext/capabilities/common/env-clean.json +14 -0
  59. higpertext/capabilities/common/env-logs.json +16 -0
  60. higpertext/capabilities/common/env-runner.json +23 -0
  61. higpertext/capabilities/common/env-status.json +13 -0
  62. higpertext/capabilities/common/env-stop.json +14 -0
  63. higpertext/capabilities/common/env-template.json +14 -0
  64. higpertext/capabilities/common/error-context-locator.json +23 -0
  65. higpertext/capabilities/common/eval-agent.json +33 -0
  66. higpertext/capabilities/common/file-map.json +17 -0
  67. higpertext/capabilities/common/governance-exception.json +54 -0
  68. higpertext/capabilities/common/graph-query.json +59 -0
  69. higpertext/capabilities/common/graph-rebuild.json +31 -0
  70. higpertext/capabilities/common/graph-visualize.json +37 -0
  71. higpertext/capabilities/common/grep-search.json +176 -0
  72. higpertext/capabilities/common/higpertext-tester.json +25 -0
  73. higpertext/capabilities/common/hook-health.json +19 -0
  74. higpertext/capabilities/common/hook-sync-check.json +19 -0
  75. higpertext/capabilities/common/hooks-manager.json +55 -0
  76. higpertext/capabilities/common/knowledge-asker.json +27 -0
  77. higpertext/capabilities/common/list-rules.json +27 -0
  78. higpertext/capabilities/common/llm-invoke.json +59 -0
  79. higpertext/capabilities/common/load-rules.json +37 -0
  80. higpertext/capabilities/common/memory-manager.json +65 -0
  81. higpertext/capabilities/common/quality-scan.json +21 -0
  82. higpertext/capabilities/common/quality-updater.json +35 -0
  83. higpertext/capabilities/common/rag-index.json +17 -0
  84. higpertext/capabilities/common/report-viewer.json +24 -0
  85. higpertext/capabilities/common/roadmap-report.json +37 -0
  86. higpertext/capabilities/common/scripts/_env_cli.py +65 -0
  87. higpertext/capabilities/common/scripts/agent_builder.py +60 -0
  88. higpertext/capabilities/common/scripts/agent_sync.py +56 -0
  89. higpertext/capabilities/common/scripts/ask_higpertext.py +38 -0
  90. higpertext/capabilities/common/scripts/code_skeletonizer.py +225 -0
  91. higpertext/capabilities/common/scripts/commit_report.py +134 -0
  92. higpertext/capabilities/common/scripts/context_assembler.py +70 -0
  93. higpertext/capabilities/common/scripts/context_budget_report.py +53 -0
  94. higpertext/capabilities/common/scripts/dep_manager.py +81 -0
  95. higpertext/capabilities/common/scripts/docs_sync.py +981 -0
  96. higpertext/capabilities/common/scripts/doctor.py +144 -0
  97. higpertext/capabilities/common/scripts/efficiency_meter.py +83 -0
  98. higpertext/capabilities/common/scripts/env_catalog.py +47 -0
  99. higpertext/capabilities/common/scripts/env_clean.py +30 -0
  100. higpertext/capabilities/common/scripts/env_logs.py +32 -0
  101. higpertext/capabilities/common/scripts/env_runner.py +53 -0
  102. higpertext/capabilities/common/scripts/env_status.py +38 -0
  103. higpertext/capabilities/common/scripts/env_stop.py +30 -0
  104. higpertext/capabilities/common/scripts/env_template.py +73 -0
  105. higpertext/capabilities/common/scripts/error_context_locator.py +138 -0
  106. higpertext/capabilities/common/scripts/eval_agent.py +80 -0
  107. higpertext/capabilities/common/scripts/file_map.py +95 -0
  108. higpertext/capabilities/common/scripts/governance_exception.py +116 -0
  109. higpertext/capabilities/common/scripts/graph_query.py +104 -0
  110. higpertext/capabilities/common/scripts/graph_rebuild.py +107 -0
  111. higpertext/capabilities/common/scripts/graph_visualize.py +76 -0
  112. higpertext/capabilities/common/scripts/grep_search.py +648 -0
  113. higpertext/capabilities/common/scripts/higpertext_tester.py +102 -0
  114. higpertext/capabilities/common/scripts/hook_health.py +149 -0
  115. higpertext/capabilities/common/scripts/hook_sync_check.py +134 -0
  116. higpertext/capabilities/common/scripts/hooks_manager.py +171 -0
  117. higpertext/capabilities/common/scripts/list_rules.py +175 -0
  118. higpertext/capabilities/common/scripts/llm_invoke.py +135 -0
  119. higpertext/capabilities/common/scripts/load_rules.py +379 -0
  120. higpertext/capabilities/common/scripts/memory_manager.py +210 -0
  121. higpertext/capabilities/common/scripts/presentation_engine.py +63 -0
  122. higpertext/capabilities/common/scripts/quality_scan.py +132 -0
  123. higpertext/capabilities/common/scripts/rag_index.py +39 -0
  124. higpertext/capabilities/common/scripts/report_viewer.py +106 -0
  125. higpertext/capabilities/common/scripts/roadmap_report.py +73 -0
  126. higpertext/capabilities/common/scripts/search_router.py +111 -0
  127. higpertext/capabilities/common/scripts/semantic_diff.py +166 -0
  128. higpertext/capabilities/common/scripts/semantic_search.py +43 -0
  129. higpertext/capabilities/common/scripts/session_control.py +136 -0
  130. higpertext/capabilities/common/scripts/smart_read.py +232 -0
  131. higpertext/capabilities/common/scripts/subagent_executor.py +143 -0
  132. higpertext/capabilities/common/scripts/sync_agents.py +353 -0
  133. higpertext/capabilities/common/scripts/task_decomposer.py +78 -0
  134. higpertext/capabilities/common/scripts/telemetry_report.py +36 -0
  135. higpertext/capabilities/common/search-router.json +24 -0
  136. higpertext/capabilities/common/semantic-diff.json +40 -0
  137. higpertext/capabilities/common/semantic-search.json +19 -0
  138. higpertext/capabilities/common/session-clean.json +20 -0
  139. higpertext/capabilities/common/session-start.json +44 -0
  140. higpertext/capabilities/common/smart-read.json +28 -0
  141. higpertext/capabilities/common/subagent-executor.json +25 -0
  142. higpertext/capabilities/common/sync-agents.json +32 -0
  143. higpertext/capabilities/common/task-decomposer.json +37 -0
  144. higpertext/capabilities/common/telemetry-report.json +23 -0
  145. higpertext/capabilities/git/__init__.py +0 -0
  146. higpertext/capabilities/git/committer.json +61 -0
  147. higpertext/capabilities/git/diff.json +33 -0
  148. higpertext/capabilities/git/ls-files.json +44 -0
  149. higpertext/capabilities/git/rm.json +27 -0
  150. higpertext/capabilities/git/scripts/__init__.py +0 -0
  151. higpertext/capabilities/git/scripts/commit_changes.py +1077 -0
  152. higpertext/capabilities/git/scripts/git_diff.py +171 -0
  153. higpertext/capabilities/git/scripts/git_ls_files.py +376 -0
  154. higpertext/capabilities/git/scripts/git_rm.py +62 -0
  155. higpertext/capabilities/security/k8s-auditor.json +33 -0
  156. higpertext/capabilities/security/scripts/k8s_auditor.py +307 -0
  157. higpertext/capabilities/security/scripts/secret_scanner.py +235 -0
  158. higpertext/capabilities/security/secret-scanner.json +32 -0
  159. higpertext/hooks/__init__.py +28 -0
  160. higpertext/hooks/_compat.py +27 -0
  161. higpertext/hooks/hook_tasks/__init__.py +1 -0
  162. higpertext/hooks/hook_tasks/_rules/__init__.py +0 -0
  163. higpertext/hooks/hook_tasks/_rules/bash_rules.py +635 -0
  164. higpertext/hooks/hook_tasks/_rules/context_engine_rule.py +79 -0
  165. higpertext/hooks/hook_tasks/_rules/context_rules.py +199 -0
  166. higpertext/hooks/hook_tasks/_rules/governance_adapter.py +72 -0
  167. higpertext/hooks/hook_tasks/_rules/profile_rules.json +25 -0
  168. higpertext/hooks/hook_tasks/_rules/quality_rules.py +86 -0
  169. higpertext/hooks/hook_tasks/_rules/security_rules.py +214 -0
  170. higpertext/hooks/hook_tasks/_rules/session_rules.py +316 -0
  171. higpertext/hooks/hook_tasks/_rules/telemetry_rules.py +121 -0
  172. higpertext/hooks/hook_tasks/audit_logger_hook.py +28 -0
  173. higpertext/hooks/hook_tasks/hook_bash_guard.py +101 -0
  174. higpertext/hooks/hook_tasks/hook_code_quality.py +48 -0
  175. higpertext/hooks/hook_tasks/hook_context_hint.py +46 -0
  176. higpertext/hooks/hook_tasks/hook_context_manager.py +44 -0
  177. higpertext/hooks/hook_tasks/hook_io.py +122 -0
  178. higpertext/hooks/hook_tasks/hook_loop_guard.py +182 -0
  179. higpertext/hooks/hook_tasks/hook_post_observer.py +54 -0
  180. higpertext/hooks/hook_tasks/hook_read_guard.py +85 -0
  181. higpertext/hooks/hook_tasks/hook_security_guard.py +81 -0
  182. higpertext/hooks/hook_tasks/hook_session_prompt.py +83 -0
  183. higpertext/hooks/hook_tasks/hook_session_stop.py +115 -0
  184. higpertext/hooks/hook_tasks/hook_utils.py +144 -0
  185. higpertext/hooks/hook_tasks/session_guard_hook.py +23 -0
  186. higpertext/hooks/hook_tasks/telemetry_utils.py +176 -0
  187. higpertext/hooks/hook_tasks/test_echo_hook.py +33 -0
  188. higpertext/hooks/hook_tasks/webhook_hook.py +54 -0
  189. higpertext/hooks/hook_tasks/workflow_runner_hook.py +49 -0
  190. higpertext/hooks/hooks_catalog.json +116 -0
  191. higpertext/kernel/__init__.py +63 -0
  192. higpertext/kernel/_compat.py +138 -0
  193. higpertext/kernel/app_config.py +117 -0
  194. higpertext/kernel/application/__init__.py +13 -0
  195. higpertext/kernel/application/agent_registry.py +102 -0
  196. higpertext/kernel/application/capability_manager.py +61 -0
  197. higpertext/kernel/application/commit_reporter.py +247 -0
  198. higpertext/kernel/application/context_builder.py +166 -0
  199. higpertext/kernel/application/context_engine.py +409 -0
  200. higpertext/kernel/application/engine.py +41 -0
  201. higpertext/kernel/application/env_runtime.py +174 -0
  202. higpertext/kernel/application/environment_manager.py +154 -0
  203. higpertext/kernel/application/governance.py +192 -0
  204. higpertext/kernel/application/hook_registry.py +102 -0
  205. higpertext/kernel/application/hook_renderer.py +720 -0
  206. higpertext/kernel/application/ports.py +49 -0
  207. higpertext/kernel/application/profile_learner.py +358 -0
  208. higpertext/kernel/application/profile_service.py +205 -0
  209. higpertext/kernel/application/profile_services.py +6 -0
  210. higpertext/kernel/application/profile_use_cases.py +93 -0
  211. higpertext/kernel/application/rag_service.py +75 -0
  212. higpertext/kernel/application/roadmap_reporter.py +178 -0
  213. higpertext/kernel/application/semantic_engine.py +258 -0
  214. higpertext/kernel/application/session_services.py +33 -0
  215. higpertext/kernel/application/skill_hook_compiler.py +85 -0
  216. higpertext/kernel/application/telemetry.py +326 -0
  217. higpertext/kernel/application/workflow_manager.py +176 -0
  218. higpertext/kernel/config_paths.py +66 -0
  219. higpertext/kernel/domain/__init__.py +12 -0
  220. higpertext/kernel/domain/agent_registry.py +23 -0
  221. higpertext/kernel/domain/commit_reporter.py +155 -0
  222. higpertext/kernel/domain/compilers.py +7 -0
  223. higpertext/kernel/domain/context_engine.py +319 -0
  224. higpertext/kernel/domain/entities.py +51 -0
  225. higpertext/kernel/domain/env_runtime.py +62 -0
  226. higpertext/kernel/domain/governance.py +198 -0
  227. higpertext/kernel/domain/hook_models.py +29 -0
  228. higpertext/kernel/domain/profile_learner.py +186 -0
  229. higpertext/kernel/domain/rag.py +70 -0
  230. higpertext/kernel/domain/repositories.py +8 -0
  231. higpertext/kernel/domain/roadmap_reporter.py +80 -0
  232. higpertext/kernel/domain/semantic_engine.py +107 -0
  233. higpertext/kernel/engine.py +42 -0
  234. higpertext/kernel/htx_resolver.py +69 -0
  235. higpertext/kernel/infrastructure/__init__.py +13 -0
  236. higpertext/kernel/infrastructure/agent_registry.py +40 -0
  237. higpertext/kernel/infrastructure/cache/capability_cache.py +319 -0
  238. higpertext/kernel/infrastructure/capability_helper.py +40 -0
  239. higpertext/kernel/infrastructure/cli/__init__.py +1 -0
  240. higpertext/kernel/infrastructure/cli/agent_commands.py +62 -0
  241. higpertext/kernel/infrastructure/cli/arguments.py +39 -0
  242. higpertext/kernel/infrastructure/cli/capability_command_builder.py +86 -0
  243. higpertext/kernel/infrastructure/cli/capability_task_service.py +234 -0
  244. higpertext/kernel/infrastructure/cli/cli_search.py +234 -0
  245. higpertext/kernel/infrastructure/cli/parameter_contracts.py +83 -0
  246. higpertext/kernel/infrastructure/cli/parser_builder.py +122 -0
  247. higpertext/kernel/infrastructure/cli/profile_commands.py +89 -0
  248. higpertext/kernel/infrastructure/cli/roadmap_commands.py +117 -0
  249. higpertext/kernel/infrastructure/cli/router.py +1110 -0
  250. higpertext/kernel/infrastructure/cli/session_commands.py +36 -0
  251. higpertext/kernel/infrastructure/cli/task_commands.py +23 -0
  252. higpertext/kernel/infrastructure/cli/task_result_reporter.py +56 -0
  253. higpertext/kernel/infrastructure/cli/workflow_commands.py +25 -0
  254. higpertext/kernel/infrastructure/compilers/__init__.py +3 -0
  255. higpertext/kernel/infrastructure/compilers/factory.py +27 -0
  256. higpertext/kernel/infrastructure/compilers/graph_compiler.py +20 -0
  257. higpertext/kernel/infrastructure/compilers/guide_compiler.py +50 -0
  258. higpertext/kernel/infrastructure/compilers/hook_compiler.py +69 -0
  259. higpertext/kernel/infrastructure/compilers/playbook_compiler.py +154 -0
  260. higpertext/kernel/infrastructure/context_engine.py +303 -0
  261. higpertext/kernel/infrastructure/database/local_vector_store.py +99 -0
  262. higpertext/kernel/infrastructure/deployment/__init__.py +1 -0
  263. higpertext/kernel/infrastructure/deployment/resource_deployer.py +283 -0
  264. higpertext/kernel/infrastructure/diagnostics/__init__.py +1 -0
  265. higpertext/kernel/infrastructure/diagnostics/health.py +191 -0
  266. higpertext/kernel/infrastructure/env_runtime.py +227 -0
  267. higpertext/kernel/infrastructure/execution/__init__.py +1 -0
  268. higpertext/kernel/infrastructure/execution/parallel.py +188 -0
  269. higpertext/kernel/infrastructure/execution/resilience.py +155 -0
  270. higpertext/kernel/infrastructure/file_repositories.py +213 -0
  271. higpertext/kernel/infrastructure/governance.py +198 -0
  272. higpertext/kernel/infrastructure/hook_config_loader.py +53 -0
  273. higpertext/kernel/infrastructure/hook_webhook_dispatcher.py +61 -0
  274. higpertext/kernel/infrastructure/hook_workflow_bridge.py +60 -0
  275. higpertext/kernel/infrastructure/llm/__init__.py +6 -0
  276. higpertext/kernel/infrastructure/llm/provider.py +46 -0
  277. higpertext/kernel/infrastructure/llm/providers/__init__.py +0 -0
  278. higpertext/kernel/infrastructure/llm/providers/anthropic_provider.py +94 -0
  279. higpertext/kernel/infrastructure/llm/providers/gemini_embeddings.py +74 -0
  280. higpertext/kernel/infrastructure/llm/providers/gemini_provider.py +101 -0
  281. higpertext/kernel/infrastructure/llm/providers/ollama_provider.py +110 -0
  282. higpertext/kernel/infrastructure/llm/providers/openai_provider.py +98 -0
  283. higpertext/kernel/infrastructure/llm/registry.py +81 -0
  284. higpertext/kernel/infrastructure/logger.py +303 -0
  285. higpertext/kernel/infrastructure/output_store.py +70 -0
  286. higpertext/kernel/infrastructure/parser/__init__.py +1 -0
  287. higpertext/kernel/infrastructure/parser/code_chunker.py +144 -0
  288. higpertext/kernel/infrastructure/parser/language/__init__.py +14 -0
  289. higpertext/kernel/infrastructure/parser/language/base.py +41 -0
  290. higpertext/kernel/infrastructure/parser/language/powershell_parser.py +35 -0
  291. higpertext/kernel/infrastructure/parser/language/python_parser.py +98 -0
  292. higpertext/kernel/infrastructure/parser/language/typescript_parser.py +91 -0
  293. higpertext/kernel/infrastructure/parser/semantic_graph.py +409 -0
  294. higpertext/kernel/infrastructure/presentation/__init__.py +1 -0
  295. higpertext/kernel/infrastructure/presentation/html_renderer.py +137 -0
  296. higpertext/kernel/infrastructure/presentation/markdown_renderer.py +84 -0
  297. higpertext/kernel/infrastructure/presentation/markdown_report_renderer.py +97 -0
  298. higpertext/kernel/infrastructure/profile_store.py +28 -0
  299. higpertext/kernel/infrastructure/semantic_engine.py +289 -0
  300. higpertext/kernel/infrastructure/telemetry_reporter.py +132 -0
  301. higpertext/kernel/infrastructure/validation/__init__.py +1 -0
  302. higpertext/kernel/infrastructure/validation/contract_validator.py +163 -0
  303. higpertext/kernel/pkg_resources.py +38 -0
  304. higpertext/kernel/session_manager.py +319 -0
  305. higpertext/templates/env/generic-shell.yaml +21 -0
  306. higpertext/templates/env/node-vitest.yaml +27 -0
  307. higpertext/templates/env/python-pytest.yaml +29 -0
  308. higpertext/templates/html/commit_body.html +20 -0
  309. higpertext/templates/html/commit_diff.html +4 -0
  310. higpertext/templates/html/commit_index.html +29 -0
  311. higpertext/templates/html/commit_layer.html +11 -0
  312. higpertext/templates/html/commit_shell.html +28 -0
  313. higpertext/templates/html/graph_visualize.html +86 -0
  314. higpertext/templates/html/roadmap_body.html +12 -0
  315. higpertext/templates/html/roadmap_phase.html +5 -0
  316. higpertext/templates/html/roadmap_shell.html +29 -0
  317. higpertext/templates/markdown/commit_report.md +18 -0
  318. higpertext/templates/markdown/efficiency_report.md +12 -0
  319. higpertext/templates/markdown/roadmap_report.md +25 -0
  320. higpertext/templates/skills/best-practices.md +7 -0
  321. higpertext/templates/skills/clean-code.md +8 -0
  322. higpertext/templates/skills/ddd-standards.md +7 -0
  323. higpertext/templates/skills/tdd-practices.md +7 -0
  324. higpertext/templates/subagents/architect.md +7 -0
  325. higpertext/templates/subagents/test-engineer.md +7 -0
  326. higpertext/templates/workflows/build.json +23 -0
  327. higpertext/templates/workflows/compact.json +21 -0
  328. higpertext/templates/workflows/plan.json +59 -0
  329. higpertext/templates/workflows/review.json +26 -0
  330. higpertext/templates/workflows/spec.json +27 -0
  331. higpertext_cli-0.8.0.dist-info/METADATA +35 -0
  332. higpertext_cli-0.8.0.dist-info/RECORD +335 -0
  333. higpertext_cli-0.8.0.dist-info/WHEEL +5 -0
  334. higpertext_cli-0.8.0.dist-info/entry_points.txt +2 -0
  335. higpertext_cli-0.8.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1077 @@
1
+ """higpertext Committer — realiza commits Git leyendo reglas de formato desde gobernanza."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import subprocess # nosec B404
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
12
+
13
+ _HERE = Path(__file__).resolve()
14
+ _PROJECT_ROOT = next(
15
+ (p for p in _HERE.parents if (p / "src/config/htx_config.json").exists()),
16
+ _HERE.parents[4],
17
+ )
18
+ sys.path.insert(0, str(_PROJECT_ROOT / "src"))
19
+ try:
20
+ from higpertext.kernel.infrastructure.output_store import OutputStore as _OutputStore
21
+
22
+ _output_store: "_OutputStore | None" = _OutputStore(_PROJECT_ROOT)
23
+ except ImportError:
24
+ _output_store = None
25
+
26
+ # Telemetría — importación opcional para no romper si el módulo no está disponible
27
+ try:
28
+ import importlib.util as _ilu
29
+
30
+ _telem_path = (
31
+ Path(__file__).resolve().parents[4]
32
+ / "src"
33
+ / "higpertext"
34
+ / "hooks"
35
+ / "hook_tasks"
36
+ / "higpertext.telemetry.telemetry_utils.py"
37
+ )
38
+ if not _telem_path.exists():
39
+ # Fallback: misma carpeta desplegada en hooks
40
+ _telem_path = Path(__file__).resolve().parent / "higpertext.telemetry.telemetry_utils.py"
41
+ _spec = _ilu.spec_from_file_location("telemetry_utils", _telem_path)
42
+ _telem = _ilu.module_from_spec(_spec)
43
+ _spec.loader.exec_module(_telem)
44
+ except Exception:
45
+ _telem = None # type: ignore
46
+
47
+ _SEP = "─" * 54
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Version bumper — soporta Python, Java, .NET, Node, Python libs
51
+ # ---------------------------------------------------------------------------
52
+
53
+ import xml.etree.ElementTree as _ET # noqa: E402
54
+ from higpertext.kernel.infrastructure.logger import get_logger
55
+ _log = get_logger()
56
+
57
+
58
+ def _detect_version_file(root: Path) -> tuple[str, Path] | tuple[None, None]:
59
+ """Detecta el archivo de versión del proyecto y retorna (tipo, path)."""
60
+ candidates = [
61
+ ("python", root / "pyproject.toml"),
62
+ ("python", root / "setup.py"),
63
+ ("python", root / "setup.cfg"),
64
+ ("node", root / "package.json"),
65
+ ("java", root / "pom.xml"),
66
+ ("dotnet", next(root.glob("**/*.csproj"), Path("__none__"))),
67
+ ("dotnet", next(root.glob("**/*.fsproj"), Path("__none__"))),
68
+ ]
69
+ for kind, path in candidates:
70
+ if path.exists():
71
+ return kind, path
72
+ return None, None
73
+
74
+
75
+ def _read_version(kind: str, path: Path) -> str | None:
76
+ text = path.read_text(encoding="utf-8")
77
+ if kind == "python":
78
+ m = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
79
+ if not m:
80
+ m = re.search(r'version\s*=\s*["\']([^"\']+)["\']', text)
81
+ return m.group(1) if m else None
82
+ if kind == "node":
83
+ try:
84
+ return json.loads(text).get("version")
85
+ except json.JSONDecodeError:
86
+ return None
87
+ if kind == "java":
88
+ try:
89
+ tree = _ET.parse(str(path))
90
+ ns = {"m": "http://maven.apache.org/POM/4.0.0"}
91
+ el = tree.getroot().find("m:version", ns) or tree.getroot().find("version")
92
+ return el.text if el is not None else None
93
+ except Exception:
94
+ return None
95
+ if kind == "dotnet":
96
+ m = re.search(r"<Version>([^<]+)</Version>", text)
97
+ return m.group(1) if m else None
98
+ return None
99
+
100
+
101
+ def _bump_semver(version: str, bump: str) -> str:
102
+ parts = version.lstrip("v").split(".")
103
+ major = int(parts[0]) if len(parts) > 0 else 0
104
+ minor = int(parts[1]) if len(parts) > 1 else 0
105
+ patch = int(parts[2].split("-")[0]) if len(parts) > 2 else 0
106
+ if bump == "major":
107
+ return f"{major + 1}.0.0"
108
+ if bump == "minor":
109
+ return f"{major}.{minor + 1}.0"
110
+ return f"{major}.{minor}.{patch + 1}"
111
+
112
+
113
+ def _bump_calver(version: str, bump: str) -> str:
114
+ from datetime import date
115
+
116
+ today = date.today()
117
+ return f"{today.year}.{today.month:02d}.{today.day:02d}"
118
+
119
+
120
+ def _bump_calver_short(version: str, bump: str) -> str:
121
+ from datetime import date
122
+
123
+ today = date.today()
124
+ parts = version.split(".")
125
+ build = int(parts[2]) + 1 if len(parts) == 3 else 1
126
+ return f"{str(today.year)[2:]}.{today.month}.{build}"
127
+
128
+
129
+ def _bump_sequential(version: str, bump: str) -> str:
130
+ try:
131
+ return str(int(version.lstrip("v")) + 1)
132
+ except ValueError:
133
+ return "1"
134
+
135
+
136
+ def _bump_custom_regex(version: str, bump: str, pattern: str, template: str) -> str:
137
+ m = re.match(pattern, version)
138
+ if not m:
139
+ return version
140
+ groups = {k: int(v) if v.isdigit() else v for k, v in m.groupdict().items()}
141
+ if bump == "major" and "major" in groups:
142
+ groups["major"] += 1
143
+ groups.update({k: 0 for k in ("minor", "patch") if k in groups})
144
+ elif bump == "minor" and "minor" in groups:
145
+ groups["minor"] += 1
146
+ if "patch" in groups:
147
+ groups["patch"] = 0
148
+ elif "patch" in groups:
149
+ groups["patch"] += 1
150
+ return template.format(**groups)
151
+
152
+
153
+ def _bump_version(
154
+ version: str,
155
+ bump: str,
156
+ scheme: str,
157
+ regex_pattern: str = "",
158
+ regex_template: str = "",
159
+ ) -> str:
160
+ if scheme == "calver":
161
+ return _bump_calver(version, bump)
162
+ if scheme == "calver_short":
163
+ return _bump_calver_short(version, bump)
164
+ if scheme == "sequential":
165
+ return _bump_sequential(version, bump)
166
+ if scheme == "custom_regex" and regex_pattern and regex_template:
167
+ return _bump_custom_regex(version, bump, regex_pattern, regex_template)
168
+ return _bump_semver(version, bump)
169
+
170
+
171
+ def _write_version(kind: str, path: Path, old: str, new: str) -> None:
172
+ text = path.read_text(encoding="utf-8")
173
+ if kind == "python":
174
+ text = re.sub(
175
+ r'(^version\s*=\s*["\'])' + re.escape(old) + r'(["\'])',
176
+ lambda m: m.group(1) + new + m.group(2),
177
+ text,
178
+ flags=re.MULTILINE,
179
+ )
180
+ elif kind == "node":
181
+ data = json.loads(text)
182
+ data["version"] = new
183
+ text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
184
+ elif kind == "java":
185
+ text = re.sub(
186
+ r"(<version>)" + re.escape(old) + r"(</version>)",
187
+ lambda m: m.group(1) + new + m.group(2),
188
+ text,
189
+ count=1,
190
+ )
191
+ elif kind == "dotnet":
192
+ text = re.sub(
193
+ r"(<Version>)" + re.escape(old) + r"(</Version>)",
194
+ lambda m: m.group(1) + new + m.group(2),
195
+ text,
196
+ count=1,
197
+ )
198
+ path.write_text(text, encoding="utf-8")
199
+
200
+
201
+ def _create_tag(version: str, message: str, annotated: bool = True) -> str:
202
+ """Crea un tag git anotado o ligero. Retorna el nombre del tag."""
203
+ tag_name = f"v{version}" if not version.startswith("v") else version
204
+ if annotated:
205
+ _run(["git", "tag", "-a", tag_name, "-m", message])
206
+ else:
207
+ _run(["git", "tag", tag_name])
208
+ return tag_name
209
+
210
+
211
+ # Defaults usados cuando guidelines_contract.json no tiene commit_format
212
+ _DEFAULT_FORMAT = {
213
+ "types": [
214
+ "feat",
215
+ "fix",
216
+ "docs",
217
+ "style",
218
+ "refactor",
219
+ "perf",
220
+ "test",
221
+ "build",
222
+ "ci",
223
+ "chore",
224
+ "revert",
225
+ ],
226
+ "max_subject_length": 72,
227
+ "max_body_line_length": 100,
228
+ "require_scope_on_types": ["feat", "fix"],
229
+ "footer_tokens": ["Closes", "Refs", "BREAKING CHANGE", "Co-authored-by"],
230
+ }
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Governance loader
235
+ # ---------------------------------------------------------------------------
236
+
237
+ _DEFAULT_TAG_CONFIG = {
238
+ "enabled": True,
239
+ "default_bump": "patch",
240
+ "commit_version_bump": True,
241
+ "bump_commit_message_template": "chore(release): bump version to {version}",
242
+ "tag_message_template": "chore(release): {version}",
243
+ "annotated": True,
244
+ }
245
+
246
+
247
+ def _load_commit_format() -> tuple[dict, dict]:
248
+ """Lee commit_format y tag_config desde guidelines_contract.json; fallback a defaults."""
249
+ candidates = [
250
+ Path(__file__).resolve().parents[4]
251
+ / "src"
252
+ / "config"
253
+ / "governance"
254
+ / "guidelines_contract.json",
255
+ Path.cwd() / "src" / "config" / "governance" / "guidelines_contract.json",
256
+ ]
257
+ for path in candidates:
258
+ if path.exists():
259
+ try:
260
+ data = json.loads(path.read_text(encoding="utf-8"))
261
+ fmt = data.get("commit_format")
262
+ tag_cfg = data.get("tag_config")
263
+ return (
264
+ {**_DEFAULT_FORMAT, **fmt} if fmt else _DEFAULT_FORMAT,
265
+ ({**_DEFAULT_TAG_CONFIG, **tag_cfg} if tag_cfg else _DEFAULT_TAG_CONFIG),
266
+ )
267
+ except (OSError, json.JSONDecodeError): # nosec B110
268
+ pass
269
+ return _DEFAULT_FORMAT, _DEFAULT_TAG_CONFIG
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Message parser
274
+ # ---------------------------------------------------------------------------
275
+
276
+ _FOOTER_PATTERN = re.compile(
277
+ r"^(Closes|Refs|BREAKING CHANGE|Co-authored-by|[A-Z][a-z]+-[a-z]+)\s*[:#]",
278
+ re.IGNORECASE,
279
+ )
280
+
281
+
282
+ def _parse_subject(subject: str) -> tuple[str, str, bool, str]:
283
+ """Extrae (type, scope, breaking, description) del subject de un commit."""
284
+ m = re.match(r"^([a-z]+)(\(([^)]+)\))?(!)?: (.+)$", subject)
285
+ if m:
286
+ return m.group(1), m.group(3) or "", bool(m.group(4)), m.group(5)
287
+ return "", "", False, ""
288
+
289
+
290
+ def _split_body_footer(lines: list[str]) -> tuple[str, str]:
291
+ """Separa body y footer a partir de la línea 3 del mensaje."""
292
+ body_lines: list[str] = []
293
+ footer_lines: list[str] = []
294
+ in_footer = False
295
+ for line in lines[2:]:
296
+ if _FOOTER_PATTERN.match(line):
297
+ in_footer = True
298
+ if in_footer:
299
+ footer_lines.append(line)
300
+ else:
301
+ body_lines.append(line)
302
+ return "\n".join(body_lines).strip(), "\n".join(footer_lines).strip()
303
+
304
+
305
+ def _parse_message(message: str) -> dict:
306
+ """Descompone un mensaje en subject, body y footer."""
307
+ lines = message.strip().splitlines()
308
+ subject = lines[0].strip() if lines else ""
309
+ commit_type, scope, breaking, description = _parse_subject(subject)
310
+ body, footer = _split_body_footer(lines)
311
+ return {
312
+ "subject": subject,
313
+ "type": commit_type,
314
+ "scope": scope,
315
+ "description": description,
316
+ "breaking": breaking,
317
+ "body": body,
318
+ "footer": footer,
319
+ }
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Governance validator
324
+ # ---------------------------------------------------------------------------
325
+
326
+
327
+ def _check_type(commit_type: str, fmt: dict) -> str | None:
328
+ valid_types = fmt.get("types", [])
329
+ if commit_type and commit_type not in valid_types:
330
+ return f"Tipo '{commit_type}' no está en los tipos permitidos: {', '.join(valid_types)}\n → Regla: commit_format.types" # noqa: E501
331
+ return None
332
+
333
+
334
+ def _check_subject_length(subject: str, fmt: dict) -> str | None:
335
+ max_subj = fmt.get("max_subject_length", 72)
336
+ if len(subject) > max_subj:
337
+ return f"Subject tiene {len(subject)} caracteres (máximo {max_subj})\n → Regla: commit_format.max_subject_length" # noqa: E501
338
+ return None
339
+
340
+
341
+ def _check_scope_required(commit_type: str, scope: str, fmt: dict) -> str | None:
342
+ require_scope = fmt.get("require_scope_on_types", [])
343
+ if commit_type in require_scope and not scope:
344
+ return f"El tipo '{commit_type}' requiere scope: {commit_type}(scope): descripción\n → Regla: commit_format.require_scope_on_types" # noqa: E501
345
+ return None
346
+
347
+
348
+ def _check_body_line_lengths(body: str, fmt: dict) -> list[str]:
349
+ max_body = fmt.get("max_body_line_length", 100)
350
+ return [
351
+ f"Línea {i} del body tiene {
352
+ len(line)} caracteres (máximo {max_body})\n → Regla: commit_format.max_body_line_length" # noqa: E501
353
+ for i, line in enumerate(body.splitlines(), 1)
354
+ if len(line) > max_body
355
+ ]
356
+
357
+
358
+ def _validate_against_governance(parsed: dict, fmt: dict) -> list[str]:
359
+ """Valida el mensaje parseado contra las reglas de commit_format."""
360
+ checks = [
361
+ _check_type(parsed["type"], fmt),
362
+ _check_subject_length(parsed["subject"], fmt),
363
+ _check_scope_required(parsed["type"], parsed["scope"], fmt),
364
+ ]
365
+ warnings = [w for w in checks if w]
366
+ warnings += _check_body_line_lengths(parsed["body"], fmt)
367
+ return warnings
368
+
369
+
370
+ # ---------------------------------------------------------------------------
371
+ # Git helpers
372
+ # ---------------------------------------------------------------------------
373
+
374
+
375
+ def _run(cmd: list[str], check: bool = True) -> tuple[int, str, str]:
376
+ res = subprocess.run(cmd, shell=False, capture_output=True, text=True) # nosec B603
377
+ if check and res.returncode != 0:
378
+ print(f"[ERROR] {' '.join(cmd)}\n{res.stderr}", file=sys.stderr)
379
+ sys.exit(res.returncode)
380
+ return res.returncode, res.stdout.strip(), res.stderr.strip()
381
+
382
+
383
+ def _get_commit_stats() -> tuple[list[str], int, int]:
384
+ """Retorna (archivos, inserciones, eliminaciones) del último commit."""
385
+ _, out, _ = _run(["git", "show", "--stat", "--format=", "HEAD"], check=False)
386
+ files: list[str] = []
387
+ insertions = deletions = 0
388
+ for line in out.splitlines():
389
+ line = line.strip()
390
+ if not line:
391
+ continue
392
+ if "changed" in line:
393
+ m = re.search(r"(\d+) insertion", line)
394
+ if m:
395
+ insertions = int(m.group(1))
396
+ m = re.search(r"(\d+) deletion", line)
397
+ if m:
398
+ deletions = int(m.group(1))
399
+ elif "|" in line:
400
+ fname = line.split("|")[0].strip()
401
+ if fname:
402
+ files.append(fname)
403
+ return files, insertions, deletions
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Output printer
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ def _print_commit_subject(parsed: dict) -> None:
412
+ if parsed["type"]:
413
+ scope_str = f"({parsed['scope']})" if parsed["scope"] else ""
414
+ breaking_str = " ⚠ BREAKING CHANGE" if parsed["breaking"] else ""
415
+ _log.info(f" Tipo : {parsed['type']}{breaking_str}")
416
+ if parsed["scope"]:
417
+ _log.info(f" Scope : {parsed['scope']}")
418
+ _log.info(f" Subject : {parsed['type']}{scope_str}: {parsed['description']}")
419
+ else:
420
+ _log.info(f" Subject : {parsed['subject']}")
421
+
422
+
423
+ def _print_multiline_section(label: str, text: str) -> None:
424
+ if text:
425
+ _log.info(_SEP)
426
+ _log.info(f" {label}:")
427
+ for line in text.splitlines():
428
+ _log.info(f" {line}")
429
+
430
+
431
+ def _print_governance_warnings(warnings: list[str]) -> None:
432
+ if warnings:
433
+ _log.info(_SEP)
434
+ _log.warning(" ⚠ Advertencias de gobernanza:")
435
+ for w in warnings:
436
+ for line in w.splitlines():
437
+ _log.info(f" {line}")
438
+
439
+
440
+ def _print_result(
441
+ parsed: dict,
442
+ branch: str,
443
+ commit_hash: str,
444
+ files: list[str],
445
+ insertions: int,
446
+ deletions: int,
447
+ warnings: list[str],
448
+ ) -> None:
449
+ _log.info()
450
+ _log.info("╔" + "═" * 54 + "╗")
451
+ _log.info("║ COMMIT REALIZADO" + " " * 36 + "║")
452
+ _log.info("╚" + "═" * 54 + "╝")
453
+ _log.info(f" Branch : {branch}")
454
+ _log.info(f" Hash : {commit_hash}")
455
+ _log.info(_SEP)
456
+ _print_commit_subject(parsed)
457
+ _print_multiline_section("Body ", parsed["body"])
458
+ _print_multiline_section("Footer ", parsed["footer"])
459
+ _print_governance_warnings(warnings)
460
+ _log.info(_SEP)
461
+ for f in files:
462
+ _log.info(f" {f}")
463
+ if not files:
464
+ _log.info(" (sin detalle de archivos)")
465
+ _log.info(_SEP)
466
+ _log.info(f" +{insertions} líneas -{deletions} líneas ({len(files)} archivo(s))")
467
+ _log.info()
468
+
469
+
470
+ # ---------------------------------------------------------------------------
471
+ # LT Commit Report — helpers
472
+ # ---------------------------------------------------------------------------
473
+
474
+
475
+ def _get_commit_diff_summary() -> list[tuple[str, str, int, int]]:
476
+ """Retorna (filepath, mode, additions, deletions) del último commit por archivo."""
477
+ _, out, _ = _run(["git", "show", "--numstat", "--format=", "HEAD"], check=False)
478
+ result = []
479
+ for line in out.splitlines():
480
+ parts = line.split("\t")
481
+ if len(parts) == 3:
482
+ try:
483
+ add = int(parts[0]) if parts[0] != "-" else 0
484
+ delete = int(parts[1]) if parts[1] != "-" else 0
485
+ result.append((parts[2], "M", add, delete))
486
+ except ValueError:
487
+ continue
488
+ return result
489
+
490
+
491
+ def _get_session_context(root: Path) -> dict:
492
+ """Lee session.json y environment.json para contexto de sesión."""
493
+ ctx: dict = {}
494
+ try:
495
+ s = json.loads(
496
+ (root / WORKSPACE_DIR_NAME / "state" / "session.json").read_text(encoding="utf-8")
497
+ )
498
+ ctx["session_id"] = s.get("session_id", "—")
499
+ ctx["profile"] = s.get("profile", "—")
500
+ ctx["skills"] = s.get("active_skills", [])
501
+ ctx["subagents"] = s.get("active_subagents", [])
502
+ except (OSError, json.JSONDecodeError): # nosec B110
503
+ pass
504
+ try:
505
+ e = json.loads(
506
+ (root / WORKSPACE_DIR_NAME / "config" / "environment.json").read_text(encoding="utf-8")
507
+ )
508
+ ctx.setdefault("profile", e.get("active_profile", "—"))
509
+ ctx["project"] = e.get("project_name", "—")
510
+ ctx["assistant"] = e.get("assistant", "—")
511
+ except (OSError, json.JSONDecodeError): # nosec B110
512
+ pass
513
+ return ctx
514
+
515
+
516
+ _REVIEW_CHECKLIST: dict[str, list[str]] = {
517
+ "feat": [
518
+ "¿Se agregaron tests unitarios para la nueva funcionalidad?",
519
+ "¿Se actualizó la documentación relevante?",
520
+ "¿El PR cumple el umbral de cobertura (≥80%)?",
521
+ "¿Se ejecutó `ado_admin.code-quality` sobre los archivos nuevos?",
522
+ ],
523
+ "fix": [
524
+ "¿El bug fue reproducido antes del fix?",
525
+ "¿Se agregó un test de regresión que falla sin el fix?",
526
+ "¿Aplica a producción? Si sí, ¿hay hotfix branch?",
527
+ "¿Se verificó con `ado_admin.code-coverage` que la línea del bug está cubierta?",
528
+ ],
529
+ "refactor": [
530
+ "¿El comportamiento observable es idéntico antes y después?",
531
+ "¿Los tests existentes siguen en verde?",
532
+ "¿El score de calidad mejoró o se mantuvo (`ado_admin.code-quality`)?",
533
+ "¿Se actualizaron los docstrings afectados?",
534
+ ],
535
+ "perf": [
536
+ "¿Hay benchmark que demuestra la mejora?",
537
+ "¿Se midió impacto en memoria además de velocidad?",
538
+ ],
539
+ "docs": [
540
+ "¿Los enlaces internos del documento son relativos y verificados?",
541
+ "¿Se ejecutó `common.docs-sync` si se modificaron capacidades/perfiles?",
542
+ ],
543
+ "ci": [
544
+ "¿Se probó el pipeline en rama feature antes de mergear?",
545
+ "¿Los tiempos de ejecución del pipeline no aumentaron significativamente?",
546
+ ],
547
+ }
548
+ _DEFAULT_CHECKLIST = [
549
+ "¿El commit es atómico (un solo cambio lógico)?",
550
+ "¿Los tests del proyecto siguen en verde?",
551
+ ]
552
+
553
+ _NEXT_STEP: dict[str, str] = {
554
+ "feat": "Abrir PR → `htx task ado_admin.pr-reviewer --pr_id <id>`",
555
+ "fix": "Verificar en staging → ejecutar `workflow.higpertext-build` sobre la rama",
556
+ "refactor": "Ejecutar quality gate → `htx task ado_admin.code-quality --path src/`",
557
+ "perf": "Ejecutar cobertura → `htx task ado_admin.code-coverage`",
558
+ "test": "Verificar cobertura global → `htx task ado_admin.code-coverage`",
559
+ "docs": "Sincronizar catálogos → `htx task common.docs-sync`",
560
+ "ci": "Monitorear pipeline → `htx task common.higpertext-tester`",
561
+ "chore": "Verificar integridad → `htx task common.higpertext-tester`",
562
+ }
563
+
564
+
565
+ # ---------------------------------------------------------------------------
566
+ # LT Commit Report
567
+ # ---------------------------------------------------------------------------
568
+
569
+ _IMPACT_MAP: dict[str, str] = {
570
+ "feat": "🟢 Nueva funcionalidad — impacto directo en el producto",
571
+ "fix": "🔴 Corrección de bug — revisar si afecta producción",
572
+ "refactor": "🔵 Refactoring — sin cambio de comportamiento observable",
573
+ "perf": "🟡 Mejora de rendimiento",
574
+ "test": "⚪ Cobertura de tests — sin cambio funcional",
575
+ "docs": "⚪ Documentación — sin cambio funcional",
576
+ "build": "🟠 Cambio de build/infraestructura",
577
+ "ci": "🟠 Cambio de pipeline CI/CD",
578
+ "chore": "⚪ Tarea de mantenimiento",
579
+ "style": "⚪ Formato de código — sin cambio lógico",
580
+ "revert": "🔴 Reversión — revisar impacto en historial",
581
+ }
582
+
583
+ _RATIONALE_TEMPLATES: dict[str, str] = {
584
+ "feat": (
585
+ "Implementación de la funcionalidad '{desc}'. "
586
+ "Esta integración expande las capacidades técnicas del asistente "
587
+ "para dar soporte a nuevos flujos de trabajo de negocio."
588
+ ),
589
+ "fix": (
590
+ "Corrección del problema '{desc}'. "
591
+ "Resuelve fallos técnicos identificados en el flujo de ejecución, "
592
+ "estabilizando la infraestructura y minimizando el impacto en la operación."
593
+ ),
594
+ "refactor": (
595
+ "Refactorización de '{desc}'. "
596
+ "Mejora la legibilidad, modularidad y apego a los principios SOLID "
597
+ "sin alterar el comportamiento observable, reduciendo la deuda técnica."
598
+ ),
599
+ "docs": (
600
+ "Actualización de documentación: '{desc}'. "
601
+ "Sincroniza y documenta contratos, guías de usuario o reportes de integración "
602
+ "para mantener la transparencia operativa y gobernanza del proyecto."
603
+ ),
604
+ }
605
+
606
+ _RATIONALE_DEFAULT = (
607
+ "Cambio técnico asociado a '{desc}'. "
608
+ "Soporta la mantenibilidad del software de acuerdo a los estándares del framework."
609
+ )
610
+
611
+
612
+ def _deduce_rationale(commit_type: str, parsed: dict) -> str:
613
+ desc = parsed.get("description", parsed.get("subject", ""))
614
+ template = _RATIONALE_TEMPLATES.get(commit_type, _RATIONALE_DEFAULT)
615
+ return template.format(desc=desc) + " *(Deducido automáticamente)*"
616
+
617
+
618
+ def _report_header_lines(
619
+ parsed: dict, commit_hash: str, branch: str, commit_type: str, breaking_flag: str
620
+ ) -> list[str]:
621
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
622
+ impact = _IMPACT_MAP.get(commit_type, "⚪ Tipo desconocido")
623
+ scope_row = f"| **Scope** | `{parsed['scope']}` |" if parsed["scope"] else "| **Scope** | — |"
624
+ return [
625
+ f"# Commit Report — `{commit_hash}`",
626
+ "",
627
+ "| Campo | Valor |",
628
+ "|-------|-------|",
629
+ f"| **Fecha** | {now} |",
630
+ f"| **Branch** | `{branch}` |",
631
+ f"| **Hash** | `{commit_hash}` |",
632
+ f"| **Tipo** | `{commit_type}`{breaking_flag} |",
633
+ scope_row,
634
+ f"| **Impacto** | {impact} |",
635
+ ]
636
+
637
+
638
+ def _report_purpose_lines(parsed: dict, scope_str: str) -> list[str]:
639
+ subject_line = (
640
+ f"**{parsed['type']}{scope_str}: {parsed['description']}**"
641
+ if parsed["type"]
642
+ else f"**{parsed['subject']}**"
643
+ )
644
+ lines = ["", "## Propósito", "", subject_line]
645
+ if parsed["body"]:
646
+ lines += ["", parsed["body"]]
647
+ if parsed["footer"]:
648
+ lines += ["", f"_{parsed['footer']}_"]
649
+ return lines
650
+
651
+
652
+ def _report_governance_lines(warnings: list[str]) -> list[str]:
653
+ if warnings:
654
+ lines = ["", "## ⚠️ Advertencias de gobernanza", ""]
655
+ for w in warnings:
656
+ lines.append(f"- {w.replace(chr(10), ' · ')}")
657
+ return lines
658
+ return [
659
+ "",
660
+ "## ✅ Gobernanza",
661
+ "",
662
+ "Sin advertencias — commit cumple todos los lineamientos.",
663
+ ]
664
+
665
+
666
+ def _report_changes_lines(files: list[str], insertions: int, deletions: int) -> list[str]:
667
+ diff_detail = _get_commit_diff_summary()
668
+ lines = [
669
+ "",
670
+ "## Cambios técnicos",
671
+ "",
672
+ "| Métrica | Valor |",
673
+ "|---------|-------|",
674
+ f"| Archivos | {len(files)} |",
675
+ f"| Líneas añadidas | +{insertions} |",
676
+ f"| Líneas eliminadas | -{deletions} |",
677
+ ]
678
+ if diff_detail:
679
+ lines += [
680
+ "",
681
+ "### Detalle por archivo",
682
+ "",
683
+ "| Archivo | +añ | -el |",
684
+ "|---------|-----|-----|",
685
+ ]
686
+ for filepath, _, add, delete in diff_detail:
687
+ lines.append(f"| `{filepath}` | +{add} | -{delete} |")
688
+ elif files:
689
+ lines += ["", "### Archivos", ""]
690
+ for f in files:
691
+ lines.append(f"- `{f}`")
692
+ return lines
693
+
694
+
695
+ def _report_session_lines(ctx: dict) -> list[str]:
696
+ if not ctx:
697
+ return []
698
+ lines = ["", "## Contexto de sesión", ""]
699
+ for key, label in [
700
+ ("project", "Proyecto"),
701
+ ("profile", "Perfil"),
702
+ ("assistant", "Asistente"),
703
+ ("session_id", "Sesión"),
704
+ ]:
705
+ if ctx.get(key):
706
+ lines.append(f"- **{label}**: `{ctx[key]}`")
707
+ if ctx.get("skills"):
708
+ lines.append(f"- **Skills activas**: {', '.join(f'`{s}`' for s in ctx['skills'])}")
709
+ if ctx.get("subagents"):
710
+ lines.append(f"- **Subagentes**: {', '.join(f'`{a}`' for a in ctx['subagents'])}")
711
+ return lines
712
+
713
+
714
+ def _report_checklist_lines(commit_type: str) -> list[str]:
715
+ checklist = _REVIEW_CHECKLIST.get(commit_type, _DEFAULT_CHECKLIST)
716
+ return ["", "## Checklist de revisión", ""] + [f"- [ ] {item}" for item in checklist]
717
+
718
+
719
+ def _report_next_step_lines(commit_type: str) -> list[str]:
720
+ next_step = _NEXT_STEP.get(
721
+ commit_type, "Verificar integridad → `htx task common.higpertext-tester`"
722
+ )
723
+ return ["", "## Siguiente paso sugerido", "", f"➡️ {next_step}", ""]
724
+
725
+
726
+ def _generate_commit_report(
727
+ parsed: dict,
728
+ branch: str,
729
+ commit_hash: str,
730
+ files: list[str],
731
+ insertions: int,
732
+ deletions: int,
733
+ warnings: list[str],
734
+ rationale: str | None = None,
735
+ ) -> str:
736
+ """Genera un reporte Markdown orientado al Lead Tech con impacto y propósito del commit."""
737
+ commit_type = parsed["type"] or "unknown"
738
+ scope_str = f"({parsed['scope']})" if parsed["scope"] else ""
739
+ breaking_flag = " — ⚠️ BREAKING CHANGE" if parsed["breaking"] else ""
740
+ effective_rationale = rationale or _deduce_rationale(commit_type, parsed)
741
+
742
+ lines = _report_header_lines(parsed, commit_hash, branch, commit_type, breaking_flag)
743
+ lines += [
744
+ "",
745
+ "## 🎯 Justificación Técnica & Decisiones Arquitectónicas",
746
+ "",
747
+ effective_rationale,
748
+ ]
749
+ lines += _report_purpose_lines(parsed, scope_str)
750
+ lines += _report_governance_lines(warnings)
751
+ lines += _report_changes_lines(files, insertions, deletions)
752
+ lines += _report_checklist_lines(commit_type)
753
+ lines += _report_session_lines(_get_session_context(Path(__file__).resolve().parents[4]))
754
+ lines += _report_next_step_lines(commit_type)
755
+ return "\n".join(lines)
756
+
757
+
758
+ # ---------------------------------------------------------------------------
759
+ # HTML report + index
760
+ # ---------------------------------------------------------------------------
761
+
762
+
763
+ def _generate_html_report(commit_hash: str, root: Path) -> Path | None:
764
+ """Llama commit_report.py para generar HTML y actualiza el índice acumulativo."""
765
+ report_script = (
766
+ root / "src" / "higpertext" / "capabilities" / "common" / "scripts" / "commit_report.py"
767
+ )
768
+ python = root / ".venv" / "bin" / "python"
769
+ out_dir = root / WORKSPACE_DIR_NAME / "reports" / "commits"
770
+ out_dir.mkdir(parents=True, exist_ok=True)
771
+ out_path = out_dir / f"{commit_hash}_report.html"
772
+ try:
773
+ subprocess.run( # nosec B603
774
+ [
775
+ str(python),
776
+ str(report_script),
777
+ "--commit",
778
+ "HEAD",
779
+ "--format",
780
+ "html",
781
+ "--output",
782
+ str(out_path),
783
+ "--diff",
784
+ ],
785
+ cwd=str(root),
786
+ capture_output=True,
787
+ timeout=30,
788
+ )
789
+ except Exception:
790
+ return None
791
+ if out_path.exists():
792
+ _update_commit_index(out_dir, root)
793
+ return out_path if out_path.exists() else None
794
+
795
+
796
+ def _update_commit_index(reports_dir: Path, root: Path) -> None:
797
+ """Regenera index.html con todos los reportes HTML de commits ordenados por fecha."""
798
+ html_files = sorted(reports_dir.glob("*_report.html"), reverse=True)
799
+ rows = ""
800
+ for f in html_files:
801
+ name = f.stem.replace("_report", "")
802
+ rows += f'<tr><td><a href="{f.name}">{name}</a></td><td>{f.stat().st_mtime:.0f}</td></tr>\n'
803
+ index = f"""<!DOCTYPE html>
804
+ <html lang="es"><head><meta charset="UTF-8">
805
+ <title>higpertext — Índice de Commits</title>
806
+ <style>body{{font-family:monospace;padding:2rem;background:#0d1117;color:#e6edf3}}
807
+ a{{color:#58a6ff}}table{{border-collapse:collapse;width:100%}}
808
+ th,td{{padding:.5rem 1rem;border:1px solid #30363d;text-align:left}}
809
+ th{{background:#161b22}}</style></head>
810
+ <body><h1>📋 Índice de Reportes de Commit</h1>
811
+ <p>Proyecto: <code>{root.name}</code> — {len(html_files)} reporte(s)</p>
812
+ <table><thead><tr><th>Commit</th><th>Timestamp</th></tr></thead>
813
+ <tbody>{rows}</tbody></table></body></html>"""
814
+ (reports_dir / "index.html").write_text(index, encoding="utf-8")
815
+
816
+
817
+ def _load_commit_history(json_path: Path) -> dict:
818
+ if json_path.exists():
819
+ try:
820
+ return json.loads(json_path.read_text(encoding="utf-8"))
821
+ except Exception: # nosec B110
822
+ pass
823
+ return {}
824
+
825
+
826
+ def _build_commit_entry(
827
+ commit_hash: str,
828
+ branch: str,
829
+ message: str,
830
+ parsed: dict,
831
+ files: list[str],
832
+ insertions: int,
833
+ deletions: int,
834
+ warnings: list,
835
+ rationale: str | None,
836
+ ) -> dict:
837
+ return {
838
+ "commit": commit_hash,
839
+ "branch": branch,
840
+ "message": message,
841
+ "type": parsed.get("type", ""),
842
+ "scope": parsed.get("scope", ""),
843
+ "description": parsed.get("description", ""),
844
+ "rationale": rationale or "",
845
+ "timestamp": datetime.now(timezone.utc).isoformat(),
846
+ "stats": {
847
+ "files_changed": len(files),
848
+ "files_list": files,
849
+ "insertions": insertions,
850
+ "deletions": deletions,
851
+ },
852
+ "warnings": warnings,
853
+ }
854
+
855
+
856
+ def _write_json_commit_report(
857
+ commit_hash: str,
858
+ root: Path,
859
+ message: str,
860
+ parsed: dict,
861
+ branch: str,
862
+ files: list[str],
863
+ insertions: int,
864
+ deletions: int,
865
+ warnings: list,
866
+ rationale: str | None,
867
+ ) -> None:
868
+ """Guarda un reporte JSON del commit en .higpertext/state/commits.json."""
869
+ state_dir = root / WORKSPACE_DIR_NAME / "state"
870
+ state_dir.mkdir(parents=True, exist_ok=True)
871
+ json_path = state_dir / "commits.json"
872
+ history = _load_commit_history(json_path)
873
+ history[commit_hash] = _build_commit_entry(
874
+ commit_hash,
875
+ branch,
876
+ message,
877
+ parsed,
878
+ files,
879
+ insertions,
880
+ deletions,
881
+ warnings,
882
+ rationale,
883
+ )
884
+ json_path.write_text(json.dumps(history, indent=2, ensure_ascii=False), encoding="utf-8")
885
+
886
+
887
+ # ---------------------------------------------------------------------------
888
+ # Entry point
889
+ # ---------------------------------------------------------------------------
890
+
891
+
892
+ def _resolve_files_to_add(files_arg: str) -> list[str]:
893
+ """Retorna lista de archivos a stagear.
894
+
895
+ Si files_arg es '.' (default), auto-detecta todos los archivos
896
+ modificados/nuevos del working tree vía git status --porcelain.
897
+ """
898
+ if files_arg != ".":
899
+ return [f.strip() for f in files_arg.replace(",", " ").split()]
900
+ _, status_out, _ = _run(["git", "status", "--porcelain"], check=False)
901
+ cwd_name = Path.cwd().name + "/"
902
+ detected: list[str] = []
903
+ for line in status_out.splitlines():
904
+ if len(line) >= 3:
905
+ path = line[3:].strip()
906
+ if path.startswith(cwd_name):
907
+ path = path[len(cwd_name) :]
908
+ detected.append(path)
909
+ return detected if detected else ["."]
910
+
911
+
912
+ def _handle_tag(
913
+ bump: str,
914
+ tag_msg: str,
915
+ root: Path,
916
+ explicit_version: str | None = None,
917
+ tag_cfg: dict | None = None,
918
+ ) -> str | None:
919
+ """Detecta versión, la incrementa (o usa explicit_version), escribe el archivo y crea el tag."""
920
+ cfg = tag_cfg or _DEFAULT_TAG_CONFIG
921
+ kind, vfile = _detect_version_file(root)
922
+ if not vfile:
923
+ print("[WARN] No se detectó archivo de versión; tag no creado.", file=sys.stderr)
924
+ return None
925
+ old_ver = _read_version(kind, vfile)
926
+ if not old_ver:
927
+ print(
928
+ f"[WARN] No se pudo leer versión de {vfile}; tag no creado.",
929
+ file=sys.stderr,
930
+ )
931
+ return None
932
+ if explicit_version:
933
+ new_ver = explicit_version.lstrip("v")
934
+ else:
935
+ scheme = cfg.get("version_scheme", "semver")
936
+ new_ver = _bump_version(
937
+ old_ver,
938
+ bump,
939
+ scheme,
940
+ cfg.get("regex_pattern", ""),
941
+ cfg.get("regex_template", ""),
942
+ )
943
+ _write_version(kind, vfile, old_ver, new_ver)
944
+ _run(["git", "add", str(vfile)])
945
+ _log.info(f" Versión : {old_ver} → {new_ver} ({vfile.name})")
946
+ tag_name = _create_tag(new_ver, tag_msg or f"chore(release): {new_ver}")
947
+ _log.info(f" Tag : {tag_name}")
948
+ return tag_name
949
+
950
+
951
+ def _build_arg_parser():
952
+ import argparse
953
+
954
+ parser = argparse.ArgumentParser(description="higpertext ADO Git Committer")
955
+ parser.add_argument("--message", required=True)
956
+ parser.add_argument("--files", default=".")
957
+ parser.add_argument("--rationale")
958
+ parser.add_argument("--branch")
959
+ parser.add_argument("--tag", action="store_true", default=False)
960
+ parser.add_argument("--bump", choices=["patch", "minor", "major"], default="patch")
961
+ parser.add_argument("--tag-message", default="")
962
+ parser.add_argument("--version", default=None)
963
+ return parser
964
+
965
+
966
+ def _do_commit(args) -> tuple[str, str, list[str], int, int]:
967
+ files_to_add = _resolve_files_to_add(args.files)
968
+ _run(["git", "add"] + files_to_add)
969
+ _run(["git", "commit", "-m", args.message])
970
+ _, branch, _ = _run(["git", "branch", "--show-current"], check=False)
971
+ _, commit_hash, _ = _run(["git", "rev-parse", "--short", "HEAD"], check=False)
972
+ files, insertions, deletions = _get_commit_stats()
973
+ return branch or "detached", commit_hash, files, insertions, deletions
974
+
975
+
976
+ def _do_tag(args, tag_cfg: dict) -> None:
977
+ bump = args.bump if args.bump != "patch" else tag_cfg.get("default_bump", "patch")
978
+ tag_msg = args.tag_message or tag_cfg.get("tag_message_template", "chore(release): {version}")
979
+ tag_name = _handle_tag(bump, tag_msg, _PROJECT_ROOT, args.version, tag_cfg)
980
+ if not tag_name:
981
+ return
982
+ _, bump_status, _ = _run(["git", "status", "--porcelain"], check=False)
983
+ if bump_status.strip() and tag_cfg.get("commit_version_bump", True):
984
+ bump_msg = tag_cfg.get(
985
+ "bump_commit_message_template", "chore(release): bump version to {version}"
986
+ ).format(version=tag_name)
987
+ _run(["git", "commit", "-m", bump_msg])
988
+ _log.ok(f"[SUCCESS] Tag '{tag_name}' creado exitosamente.")
989
+
990
+
991
+ def _do_reports(
992
+ args,
993
+ parsed: dict,
994
+ branch_name: str,
995
+ commit_hash: str,
996
+ files: list,
997
+ insertions: int,
998
+ deletions: int,
999
+ warnings: list,
1000
+ ) -> None:
1001
+ _generate_html_report(commit_hash, _PROJECT_ROOT)
1002
+ _write_json_commit_report(
1003
+ commit_hash,
1004
+ _PROJECT_ROOT,
1005
+ args.message,
1006
+ parsed,
1007
+ branch_name,
1008
+ files,
1009
+ insertions,
1010
+ deletions,
1011
+ warnings,
1012
+ args.rationale,
1013
+ )
1014
+ report_md = _generate_commit_report(
1015
+ parsed,
1016
+ branch_name,
1017
+ commit_hash,
1018
+ files,
1019
+ insertions,
1020
+ deletions,
1021
+ warnings,
1022
+ args.rationale,
1023
+ )
1024
+ if _output_store:
1025
+ _output_store.write("ado_admin.committer", report_md)
1026
+
1027
+
1028
+ def _do_telemetry(commit_hash: str, parsed: dict) -> None:
1029
+ if not _telem:
1030
+ return
1031
+ try:
1032
+ root = Path(__file__).resolve().parents[4]
1033
+ session_data = json.loads(
1034
+ (root / WORKSPACE_DIR_NAME / "state" / "session.json").read_text(encoding="utf-8")
1035
+ )
1036
+ env_data = json.loads(
1037
+ (root / WORKSPACE_DIR_NAME / "config" / "environment.json").read_text(encoding="utf-8")
1038
+ )
1039
+ _telem.commit_event(
1040
+ root,
1041
+ session_id=session_data.get("session_id", "unknown"),
1042
+ commit_hash=commit_hash,
1043
+ commit_type=parsed.get("type", "unknown"),
1044
+ scope=parsed.get("scope", ""),
1045
+ profile=env_data.get("active_profile", "global"),
1046
+ )
1047
+ except Exception: # nosec B110
1048
+ pass
1049
+
1050
+
1051
+ def main() -> None:
1052
+ args = _build_arg_parser().parse_args()
1053
+ fmt, tag_cfg = _load_commit_format()
1054
+ parsed = _parse_message(args.message)
1055
+ warnings = _validate_against_governance(parsed, fmt)
1056
+
1057
+ _, status_out, _ = _run(["git", "status", "--porcelain"], check=False)
1058
+ if not status_out:
1059
+ _log.info("[*] No hay cambios pendientes para commitear.")
1060
+ sys.exit(0)
1061
+
1062
+ branch_name, commit_hash, files, insertions, deletions = _do_commit(args)
1063
+ _print_result(parsed, branch_name, commit_hash, files, insertions, deletions, warnings)
1064
+ _log.ok("[SUCCESS] Commit realizado exitosamente.")
1065
+
1066
+ if args.tag:
1067
+ _do_tag(args, tag_cfg)
1068
+ if args.branch:
1069
+ _run(["git", "push", "origin", args.branch])
1070
+ _log.ok(f"[SUCCESS] Push realizado a origin/{args.branch}.")
1071
+
1072
+ _do_reports(args, parsed, branch_name, commit_hash, files, insertions, deletions, warnings)
1073
+ _do_telemetry(commit_hash, parsed)
1074
+
1075
+
1076
+ if __name__ == "__main__":
1077
+ main()