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.
- config/adapters_config.json +450 -0
- config/antigravity_agent_template.json +31 -0
- config/app_config.json +174 -0
- config/context_engine.json +33 -0
- config/environments/model_defaults.json +5 -0
- config/governance/branching_strategy.json +36 -0
- config/governance/deployment_gates.json +30 -0
- config/governance/guidelines_contract.json +54 -0
- config/governance/quality_gates.json +39 -0
- config/governance/section_rules.json +22 -0
- config/governance/security_guardrails.json +52 -0
- config/hooks/README.md +35 -0
- config/hooks/custom/test_output_limiter.json +9 -0
- config/hooks/global/session_prompt.json +9 -0
- config/htx_config.json +24 -0
- config/profile_learner.json +18 -0
- config/profiles/base_agent.json +40 -0
- config/profiles/base_auditor.json +19 -0
- config/profiles/base_developer.json +19 -0
- config/profiles/base_operator.json +16 -0
- config/profiles/global.json +33 -0
- config/profiles/software_developer.json +23 -0
- config/router_content.json +137 -0
- config/semantic_graph.json +66 -0
- config/workflows/ado_release_flow.json +38 -0
- config/workflows/docs-update.json +33 -0
- config/workflows/governance-check.yaml +26 -0
- config/workflows/guidelines-sync.json +40 -0
- config/workflows/higpertext-build.json +73 -0
- config/workflows/higpertext-plan.json +38 -0
- config/workflows/higpertext-review.json +41 -0
- config/workflows/pr-quality-check.json +56 -0
- config/workflows/quality-remediation.json +57 -0
- higpertext/__init__.py +18 -0
- higpertext/adapters/__init__.py +27 -0
- higpertext/adapters/adapter_utils.py +604 -0
- higpertext/adapters/claude_adapter/__init__.py +0 -0
- higpertext/adapters/claude_adapter/claude_adapter.py +154 -0
- higpertext/adapters/copilot_adapter/__init__.py +0 -0
- higpertext/adapters/copilot_adapter/copilot_adapter.py +231 -0
- higpertext/adapters/gemini_adapter/__init__.py +0 -0
- higpertext/adapters/gemini_adapter/gemini_adapter.py +211 -0
- higpertext/adapters/llm_formatter.py +46 -0
- higpertext/adapters/open_code_adapter/__init__.py +0 -0
- higpertext/adapters/open_code_adapter/open_code_adapter.py +480 -0
- higpertext/capabilities/capabilities_runner.py +216 -0
- higpertext/capabilities/common/agent-builder.json +54 -0
- higpertext/capabilities/common/agent-sync.json +34 -0
- higpertext/capabilities/common/code-skeletonizer.json +35 -0
- higpertext/capabilities/common/commit-report.json +42 -0
- higpertext/capabilities/common/context-assembler.json +37 -0
- higpertext/capabilities/common/context-budget-report.json +15 -0
- higpertext/capabilities/common/dep-manager.json +43 -0
- higpertext/capabilities/common/docs-sync.json +14 -0
- higpertext/capabilities/common/doctor.json +18 -0
- higpertext/capabilities/common/efficiency-meter.json +31 -0
- higpertext/capabilities/common/env-catalog.json +13 -0
- higpertext/capabilities/common/env-clean.json +14 -0
- higpertext/capabilities/common/env-logs.json +16 -0
- higpertext/capabilities/common/env-runner.json +23 -0
- higpertext/capabilities/common/env-status.json +13 -0
- higpertext/capabilities/common/env-stop.json +14 -0
- higpertext/capabilities/common/env-template.json +14 -0
- higpertext/capabilities/common/error-context-locator.json +23 -0
- higpertext/capabilities/common/eval-agent.json +33 -0
- higpertext/capabilities/common/file-map.json +17 -0
- higpertext/capabilities/common/governance-exception.json +54 -0
- higpertext/capabilities/common/graph-query.json +59 -0
- higpertext/capabilities/common/graph-rebuild.json +31 -0
- higpertext/capabilities/common/graph-visualize.json +37 -0
- higpertext/capabilities/common/grep-search.json +176 -0
- higpertext/capabilities/common/higpertext-tester.json +25 -0
- higpertext/capabilities/common/hook-health.json +19 -0
- higpertext/capabilities/common/hook-sync-check.json +19 -0
- higpertext/capabilities/common/hooks-manager.json +55 -0
- higpertext/capabilities/common/knowledge-asker.json +27 -0
- higpertext/capabilities/common/list-rules.json +27 -0
- higpertext/capabilities/common/llm-invoke.json +59 -0
- higpertext/capabilities/common/load-rules.json +37 -0
- higpertext/capabilities/common/memory-manager.json +65 -0
- higpertext/capabilities/common/quality-scan.json +21 -0
- higpertext/capabilities/common/quality-updater.json +35 -0
- higpertext/capabilities/common/rag-index.json +17 -0
- higpertext/capabilities/common/report-viewer.json +24 -0
- higpertext/capabilities/common/roadmap-report.json +37 -0
- higpertext/capabilities/common/scripts/_env_cli.py +65 -0
- higpertext/capabilities/common/scripts/agent_builder.py +60 -0
- higpertext/capabilities/common/scripts/agent_sync.py +56 -0
- higpertext/capabilities/common/scripts/ask_higpertext.py +38 -0
- higpertext/capabilities/common/scripts/code_skeletonizer.py +225 -0
- higpertext/capabilities/common/scripts/commit_report.py +134 -0
- higpertext/capabilities/common/scripts/context_assembler.py +70 -0
- higpertext/capabilities/common/scripts/context_budget_report.py +53 -0
- higpertext/capabilities/common/scripts/dep_manager.py +81 -0
- higpertext/capabilities/common/scripts/docs_sync.py +981 -0
- higpertext/capabilities/common/scripts/doctor.py +144 -0
- higpertext/capabilities/common/scripts/efficiency_meter.py +83 -0
- higpertext/capabilities/common/scripts/env_catalog.py +47 -0
- higpertext/capabilities/common/scripts/env_clean.py +30 -0
- higpertext/capabilities/common/scripts/env_logs.py +32 -0
- higpertext/capabilities/common/scripts/env_runner.py +53 -0
- higpertext/capabilities/common/scripts/env_status.py +38 -0
- higpertext/capabilities/common/scripts/env_stop.py +30 -0
- higpertext/capabilities/common/scripts/env_template.py +73 -0
- higpertext/capabilities/common/scripts/error_context_locator.py +138 -0
- higpertext/capabilities/common/scripts/eval_agent.py +80 -0
- higpertext/capabilities/common/scripts/file_map.py +95 -0
- higpertext/capabilities/common/scripts/governance_exception.py +116 -0
- higpertext/capabilities/common/scripts/graph_query.py +104 -0
- higpertext/capabilities/common/scripts/graph_rebuild.py +107 -0
- higpertext/capabilities/common/scripts/graph_visualize.py +76 -0
- higpertext/capabilities/common/scripts/grep_search.py +648 -0
- higpertext/capabilities/common/scripts/higpertext_tester.py +102 -0
- higpertext/capabilities/common/scripts/hook_health.py +149 -0
- higpertext/capabilities/common/scripts/hook_sync_check.py +134 -0
- higpertext/capabilities/common/scripts/hooks_manager.py +171 -0
- higpertext/capabilities/common/scripts/list_rules.py +175 -0
- higpertext/capabilities/common/scripts/llm_invoke.py +135 -0
- higpertext/capabilities/common/scripts/load_rules.py +379 -0
- higpertext/capabilities/common/scripts/memory_manager.py +210 -0
- higpertext/capabilities/common/scripts/presentation_engine.py +63 -0
- higpertext/capabilities/common/scripts/quality_scan.py +132 -0
- higpertext/capabilities/common/scripts/rag_index.py +39 -0
- higpertext/capabilities/common/scripts/report_viewer.py +106 -0
- higpertext/capabilities/common/scripts/roadmap_report.py +73 -0
- higpertext/capabilities/common/scripts/search_router.py +111 -0
- higpertext/capabilities/common/scripts/semantic_diff.py +166 -0
- higpertext/capabilities/common/scripts/semantic_search.py +43 -0
- higpertext/capabilities/common/scripts/session_control.py +136 -0
- higpertext/capabilities/common/scripts/smart_read.py +232 -0
- higpertext/capabilities/common/scripts/subagent_executor.py +143 -0
- higpertext/capabilities/common/scripts/sync_agents.py +353 -0
- higpertext/capabilities/common/scripts/task_decomposer.py +78 -0
- higpertext/capabilities/common/scripts/telemetry_report.py +36 -0
- higpertext/capabilities/common/search-router.json +24 -0
- higpertext/capabilities/common/semantic-diff.json +40 -0
- higpertext/capabilities/common/semantic-search.json +19 -0
- higpertext/capabilities/common/session-clean.json +20 -0
- higpertext/capabilities/common/session-start.json +44 -0
- higpertext/capabilities/common/smart-read.json +28 -0
- higpertext/capabilities/common/subagent-executor.json +25 -0
- higpertext/capabilities/common/sync-agents.json +32 -0
- higpertext/capabilities/common/task-decomposer.json +37 -0
- higpertext/capabilities/common/telemetry-report.json +23 -0
- higpertext/capabilities/git/__init__.py +0 -0
- higpertext/capabilities/git/committer.json +61 -0
- higpertext/capabilities/git/diff.json +33 -0
- higpertext/capabilities/git/ls-files.json +44 -0
- higpertext/capabilities/git/rm.json +27 -0
- higpertext/capabilities/git/scripts/__init__.py +0 -0
- higpertext/capabilities/git/scripts/commit_changes.py +1077 -0
- higpertext/capabilities/git/scripts/git_diff.py +171 -0
- higpertext/capabilities/git/scripts/git_ls_files.py +376 -0
- higpertext/capabilities/git/scripts/git_rm.py +62 -0
- higpertext/capabilities/security/k8s-auditor.json +33 -0
- higpertext/capabilities/security/scripts/k8s_auditor.py +307 -0
- higpertext/capabilities/security/scripts/secret_scanner.py +235 -0
- higpertext/capabilities/security/secret-scanner.json +32 -0
- higpertext/hooks/__init__.py +28 -0
- higpertext/hooks/_compat.py +27 -0
- higpertext/hooks/hook_tasks/__init__.py +1 -0
- higpertext/hooks/hook_tasks/_rules/__init__.py +0 -0
- higpertext/hooks/hook_tasks/_rules/bash_rules.py +635 -0
- higpertext/hooks/hook_tasks/_rules/context_engine_rule.py +79 -0
- higpertext/hooks/hook_tasks/_rules/context_rules.py +199 -0
- higpertext/hooks/hook_tasks/_rules/governance_adapter.py +72 -0
- higpertext/hooks/hook_tasks/_rules/profile_rules.json +25 -0
- higpertext/hooks/hook_tasks/_rules/quality_rules.py +86 -0
- higpertext/hooks/hook_tasks/_rules/security_rules.py +214 -0
- higpertext/hooks/hook_tasks/_rules/session_rules.py +316 -0
- higpertext/hooks/hook_tasks/_rules/telemetry_rules.py +121 -0
- higpertext/hooks/hook_tasks/audit_logger_hook.py +28 -0
- higpertext/hooks/hook_tasks/hook_bash_guard.py +101 -0
- higpertext/hooks/hook_tasks/hook_code_quality.py +48 -0
- higpertext/hooks/hook_tasks/hook_context_hint.py +46 -0
- higpertext/hooks/hook_tasks/hook_context_manager.py +44 -0
- higpertext/hooks/hook_tasks/hook_io.py +122 -0
- higpertext/hooks/hook_tasks/hook_loop_guard.py +182 -0
- higpertext/hooks/hook_tasks/hook_post_observer.py +54 -0
- higpertext/hooks/hook_tasks/hook_read_guard.py +85 -0
- higpertext/hooks/hook_tasks/hook_security_guard.py +81 -0
- higpertext/hooks/hook_tasks/hook_session_prompt.py +83 -0
- higpertext/hooks/hook_tasks/hook_session_stop.py +115 -0
- higpertext/hooks/hook_tasks/hook_utils.py +144 -0
- higpertext/hooks/hook_tasks/session_guard_hook.py +23 -0
- higpertext/hooks/hook_tasks/telemetry_utils.py +176 -0
- higpertext/hooks/hook_tasks/test_echo_hook.py +33 -0
- higpertext/hooks/hook_tasks/webhook_hook.py +54 -0
- higpertext/hooks/hook_tasks/workflow_runner_hook.py +49 -0
- higpertext/hooks/hooks_catalog.json +116 -0
- higpertext/kernel/__init__.py +63 -0
- higpertext/kernel/_compat.py +138 -0
- higpertext/kernel/app_config.py +117 -0
- higpertext/kernel/application/__init__.py +13 -0
- higpertext/kernel/application/agent_registry.py +102 -0
- higpertext/kernel/application/capability_manager.py +61 -0
- higpertext/kernel/application/commit_reporter.py +247 -0
- higpertext/kernel/application/context_builder.py +166 -0
- higpertext/kernel/application/context_engine.py +409 -0
- higpertext/kernel/application/engine.py +41 -0
- higpertext/kernel/application/env_runtime.py +174 -0
- higpertext/kernel/application/environment_manager.py +154 -0
- higpertext/kernel/application/governance.py +192 -0
- higpertext/kernel/application/hook_registry.py +102 -0
- higpertext/kernel/application/hook_renderer.py +720 -0
- higpertext/kernel/application/ports.py +49 -0
- higpertext/kernel/application/profile_learner.py +358 -0
- higpertext/kernel/application/profile_service.py +205 -0
- higpertext/kernel/application/profile_services.py +6 -0
- higpertext/kernel/application/profile_use_cases.py +93 -0
- higpertext/kernel/application/rag_service.py +75 -0
- higpertext/kernel/application/roadmap_reporter.py +178 -0
- higpertext/kernel/application/semantic_engine.py +258 -0
- higpertext/kernel/application/session_services.py +33 -0
- higpertext/kernel/application/skill_hook_compiler.py +85 -0
- higpertext/kernel/application/telemetry.py +326 -0
- higpertext/kernel/application/workflow_manager.py +176 -0
- higpertext/kernel/config_paths.py +66 -0
- higpertext/kernel/domain/__init__.py +12 -0
- higpertext/kernel/domain/agent_registry.py +23 -0
- higpertext/kernel/domain/commit_reporter.py +155 -0
- higpertext/kernel/domain/compilers.py +7 -0
- higpertext/kernel/domain/context_engine.py +319 -0
- higpertext/kernel/domain/entities.py +51 -0
- higpertext/kernel/domain/env_runtime.py +62 -0
- higpertext/kernel/domain/governance.py +198 -0
- higpertext/kernel/domain/hook_models.py +29 -0
- higpertext/kernel/domain/profile_learner.py +186 -0
- higpertext/kernel/domain/rag.py +70 -0
- higpertext/kernel/domain/repositories.py +8 -0
- higpertext/kernel/domain/roadmap_reporter.py +80 -0
- higpertext/kernel/domain/semantic_engine.py +107 -0
- higpertext/kernel/engine.py +42 -0
- higpertext/kernel/htx_resolver.py +69 -0
- higpertext/kernel/infrastructure/__init__.py +13 -0
- higpertext/kernel/infrastructure/agent_registry.py +40 -0
- higpertext/kernel/infrastructure/cache/capability_cache.py +319 -0
- higpertext/kernel/infrastructure/capability_helper.py +40 -0
- higpertext/kernel/infrastructure/cli/__init__.py +1 -0
- higpertext/kernel/infrastructure/cli/agent_commands.py +62 -0
- higpertext/kernel/infrastructure/cli/arguments.py +39 -0
- higpertext/kernel/infrastructure/cli/capability_command_builder.py +86 -0
- higpertext/kernel/infrastructure/cli/capability_task_service.py +234 -0
- higpertext/kernel/infrastructure/cli/cli_search.py +234 -0
- higpertext/kernel/infrastructure/cli/parameter_contracts.py +83 -0
- higpertext/kernel/infrastructure/cli/parser_builder.py +122 -0
- higpertext/kernel/infrastructure/cli/profile_commands.py +89 -0
- higpertext/kernel/infrastructure/cli/roadmap_commands.py +117 -0
- higpertext/kernel/infrastructure/cli/router.py +1110 -0
- higpertext/kernel/infrastructure/cli/session_commands.py +36 -0
- higpertext/kernel/infrastructure/cli/task_commands.py +23 -0
- higpertext/kernel/infrastructure/cli/task_result_reporter.py +56 -0
- higpertext/kernel/infrastructure/cli/workflow_commands.py +25 -0
- higpertext/kernel/infrastructure/compilers/__init__.py +3 -0
- higpertext/kernel/infrastructure/compilers/factory.py +27 -0
- higpertext/kernel/infrastructure/compilers/graph_compiler.py +20 -0
- higpertext/kernel/infrastructure/compilers/guide_compiler.py +50 -0
- higpertext/kernel/infrastructure/compilers/hook_compiler.py +69 -0
- higpertext/kernel/infrastructure/compilers/playbook_compiler.py +154 -0
- higpertext/kernel/infrastructure/context_engine.py +303 -0
- higpertext/kernel/infrastructure/database/local_vector_store.py +99 -0
- higpertext/kernel/infrastructure/deployment/__init__.py +1 -0
- higpertext/kernel/infrastructure/deployment/resource_deployer.py +283 -0
- higpertext/kernel/infrastructure/diagnostics/__init__.py +1 -0
- higpertext/kernel/infrastructure/diagnostics/health.py +191 -0
- higpertext/kernel/infrastructure/env_runtime.py +227 -0
- higpertext/kernel/infrastructure/execution/__init__.py +1 -0
- higpertext/kernel/infrastructure/execution/parallel.py +188 -0
- higpertext/kernel/infrastructure/execution/resilience.py +155 -0
- higpertext/kernel/infrastructure/file_repositories.py +213 -0
- higpertext/kernel/infrastructure/governance.py +198 -0
- higpertext/kernel/infrastructure/hook_config_loader.py +53 -0
- higpertext/kernel/infrastructure/hook_webhook_dispatcher.py +61 -0
- higpertext/kernel/infrastructure/hook_workflow_bridge.py +60 -0
- higpertext/kernel/infrastructure/llm/__init__.py +6 -0
- higpertext/kernel/infrastructure/llm/provider.py +46 -0
- higpertext/kernel/infrastructure/llm/providers/__init__.py +0 -0
- higpertext/kernel/infrastructure/llm/providers/anthropic_provider.py +94 -0
- higpertext/kernel/infrastructure/llm/providers/gemini_embeddings.py +74 -0
- higpertext/kernel/infrastructure/llm/providers/gemini_provider.py +101 -0
- higpertext/kernel/infrastructure/llm/providers/ollama_provider.py +110 -0
- higpertext/kernel/infrastructure/llm/providers/openai_provider.py +98 -0
- higpertext/kernel/infrastructure/llm/registry.py +81 -0
- higpertext/kernel/infrastructure/logger.py +303 -0
- higpertext/kernel/infrastructure/output_store.py +70 -0
- higpertext/kernel/infrastructure/parser/__init__.py +1 -0
- higpertext/kernel/infrastructure/parser/code_chunker.py +144 -0
- higpertext/kernel/infrastructure/parser/language/__init__.py +14 -0
- higpertext/kernel/infrastructure/parser/language/base.py +41 -0
- higpertext/kernel/infrastructure/parser/language/powershell_parser.py +35 -0
- higpertext/kernel/infrastructure/parser/language/python_parser.py +98 -0
- higpertext/kernel/infrastructure/parser/language/typescript_parser.py +91 -0
- higpertext/kernel/infrastructure/parser/semantic_graph.py +409 -0
- higpertext/kernel/infrastructure/presentation/__init__.py +1 -0
- higpertext/kernel/infrastructure/presentation/html_renderer.py +137 -0
- higpertext/kernel/infrastructure/presentation/markdown_renderer.py +84 -0
- higpertext/kernel/infrastructure/presentation/markdown_report_renderer.py +97 -0
- higpertext/kernel/infrastructure/profile_store.py +28 -0
- higpertext/kernel/infrastructure/semantic_engine.py +289 -0
- higpertext/kernel/infrastructure/telemetry_reporter.py +132 -0
- higpertext/kernel/infrastructure/validation/__init__.py +1 -0
- higpertext/kernel/infrastructure/validation/contract_validator.py +163 -0
- higpertext/kernel/pkg_resources.py +38 -0
- higpertext/kernel/session_manager.py +319 -0
- higpertext/templates/env/generic-shell.yaml +21 -0
- higpertext/templates/env/node-vitest.yaml +27 -0
- higpertext/templates/env/python-pytest.yaml +29 -0
- higpertext/templates/html/commit_body.html +20 -0
- higpertext/templates/html/commit_diff.html +4 -0
- higpertext/templates/html/commit_index.html +29 -0
- higpertext/templates/html/commit_layer.html +11 -0
- higpertext/templates/html/commit_shell.html +28 -0
- higpertext/templates/html/graph_visualize.html +86 -0
- higpertext/templates/html/roadmap_body.html +12 -0
- higpertext/templates/html/roadmap_phase.html +5 -0
- higpertext/templates/html/roadmap_shell.html +29 -0
- higpertext/templates/markdown/commit_report.md +18 -0
- higpertext/templates/markdown/efficiency_report.md +12 -0
- higpertext/templates/markdown/roadmap_report.md +25 -0
- higpertext/templates/skills/best-practices.md +7 -0
- higpertext/templates/skills/clean-code.md +8 -0
- higpertext/templates/skills/ddd-standards.md +7 -0
- higpertext/templates/skills/tdd-practices.md +7 -0
- higpertext/templates/subagents/architect.md +7 -0
- higpertext/templates/subagents/test-engineer.md +7 -0
- higpertext/templates/workflows/build.json +23 -0
- higpertext/templates/workflows/compact.json +21 -0
- higpertext/templates/workflows/plan.json +59 -0
- higpertext/templates/workflows/review.json +26 -0
- higpertext/templates/workflows/spec.json +27 -0
- higpertext_cli-0.8.0.dist-info/METADATA +35 -0
- higpertext_cli-0.8.0.dist-info/RECORD +335 -0
- higpertext_cli-0.8.0.dist-info/WHEEL +5 -0
- higpertext_cli-0.8.0.dist-info/entry_points.txt +2 -0
- 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()
|