#!/usr/bin/env python3 """ Particiona CHANGELOG.md raíz por proyecto/intención. Cada sección `### tipo(componente):` se rutea a `docs/changelog/.md` según el componente. CHANGELOG.md raíz queda como índice apuntando a las particiones. Reglas de ruteo: brahman-* → protocol ente-zero|kernel|soma|... → init ente-bus|cas|wasm|brain|echo → runtime ente-*-compat|policy-provider → compat minga-* → minga yahweh-*|nahual-* → nahual lapaloma-*|pineal-* → pineal nakui-* → nakui nouser-*|akasha-* → akasha shipote-*|shuma-* → shuma gioser-* → gioser pluma-* → pluma vista-* → vista barra-* → barra cosmobiologia-* → cosmobiologia """ import re, sys from pathlib import Path from collections import defaultdict ROOT = Path("/home/sergio/brahman") CHANGELOG = ROOT / "CHANGELOG.md" OUT_DIR = ROOT / "docs" / "changelog" INIT_NAMES = {"ente-zero", "ente-kernel", "ente-soma", "ente-snapshot", "ente-incarnate"} RUNTIME_NAMES = {"ente-bus", "ente-cas", "ente-wasm", "ente-brain", "ente-echo"} PROTOCOL_BARE = {"card", "cards", "sidecar", "handshake", "broker", "admin", "net", "card-wit", "core", "arje"} def route_single(c: str) -> str | None: """Devuelve bucket o None si no matchea.""" c = c.strip() if c.startswith("brahman-"): return "protocol" if c in PROTOCOL_BARE: return "protocol" if c in INIT_NAMES: return "init" if c in RUNTIME_NAMES: return "runtime" if c.startswith("ente-") and (c.endswith("-compat") or c == "ente-policy-provider"): return "compat" if c.startswith("ente-"): return "init" if c.startswith("minga-") or c == "minga": return "minga" if c.startswith("yahweh-") or c.startswith("nahual-") or c in {"yahweh", "nahual"}: return "nahual" if c.startswith("lapaloma-") or c.startswith("pineal-") or c in {"lapaloma", "pineal"}: return "pineal" if c.startswith("nakui-") or c == "nakui": return "nakui" if c.startswith("nouser-") or c.startswith("akasha-") or c in {"nouser", "akasha", "nous", "nous-real", "nous-mock"}: return "akasha" if c.startswith("shipote-") or c.startswith("shuma-") or c in {"shipote", "shuma"}: return "shuma" if c.startswith("gioser-") or c == "gioser": return "gioser" if c.startswith("pluma-") or c == "pluma": return "pluma" if c.startswith("vista-") or c == "vista": return "vista" if c.startswith("barra-") or c == "barra": return "barra" if c.startswith("cosmobiologia-") or c == "cosmobiologia": return "cosmobiologia" # explorer + daemon: dominio variable. "explorer" solo sugiere apps/X-explorer. if c == "explorer" or c == "daemon": return None # forzar inspección de otro componente return None def route(components: str) -> str: """Componentes pueden venir como 'a+b' o 'a,b' o 'a'. Buscamos el primero que matchee un bucket conocido.""" parts = re.split(r"[+,]", components) for p in parts: b = route_single(p) if b is not None: return b return "misc" # Notas de header por proyecto (incluyen el rename si aplica). HEADERS = { "protocol": "# Changelog — protocol/\n\nContratos canónicos + routing entre módulos. Antes: `core/brahman-*` + `shared/brahman-*`.\n", "init": "# Changelog — init/\n\nInit (PID 1) + encarnación Linux. Antes: `core/ente-{zero,kernel,soma,snapshot}` + `shared/ente-incarnate`.\n", "runtime": "# Changelog — runtime/\n\nInfraestructura de ejecución (bus + cas + wasm + brain + echo). Antes: `core/ente-{bus,cas,wasm,brain,echo}`.\n", "compat": "# Changelog — compat/\n\nShims D-Bus systemd. Antes: `core/ente-*-compat`.\n", "minga": "# Changelog — minga (semantic_dht)\n", "nahual": "# Changelog — nahual\n\nMotor GPUI: libs + widgets. Renombrado de `yahweh` el 2026-05-19.\n", "pineal": "# Changelog — pineal\n\nData-viz agnóstica con backends. Renombrado de `lapaloma` el 2026-05-19; promovido fuera de `ui_engine/`.\n", "nakui": "# Changelog — nakui\n\nERP categórico.\n", "akasha": "# Changelog — akasha\n\nExplorador semántico de Mónadas. Renombrado de `nouser` el 2026-05-19.\n", "shuma": "# Changelog — shuma\n\nRuntime de espacios aislados. Renombrado de `shipote` el 2026-05-19.\n", "gioser": "# Changelog — gioser\n\nLanding WASM (chacana + 4 elementos).\n", "pluma": "# Changelog — pluma\n\nMarkdown agnóstico + visor web.\n", "vista": "# Changelog — vista\n\nDeck horizontal swipe (PageView).\n", "barra": "# Changelog — barra\n\nTaskbar agnóstica (Windows-like).\n", "cosmobiologia": "# Changelog — cosmobiologia\n\nEstudio de astrología profesional.\n", "misc": "# Changelog — misc\n\nEntradas que no encajan en otro proyecto (chore, ci, gitignore, etc.).\n", } def main(): text = CHANGELOG.read_text() lines = text.splitlines(keepends=True) # 1) Captura todas las secciones que empiezan con `### ` header_re = re.compile(r"^### (?:feat|fix|refactor|docs|chore|test|perf|build|ci|style)\(([^)]+)\):") sections: list[tuple[int, str]] = [] # (line_index, component) for i, line in enumerate(lines): m = header_re.match(line) if m: sections.append((i, m.group(1))) # 2) Construye los bloques: desde header hasta siguiente header (excl). buckets: dict[str, list[str]] = defaultdict(list) for j, (idx, comp) in enumerate(sections): end = sections[j + 1][0] if j + 1 < len(sections) else len(lines) block = "".join(lines[idx:end]).rstrip() + "\n\n" proj = route(comp) buckets[proj].append(block) # 3) Escribe particiones OUT_DIR.mkdir(parents=True, exist_ok=True) for proj, blocks in sorted(buckets.items()): path = OUT_DIR / f"{proj}.md" content = HEADERS.get(proj, f"# Changelog — {proj}\n") + "\n" content += "".join(blocks) path.write_text(content) print(f" {path.relative_to(ROOT)}: {len(blocks)} entradas") # 4) Reescribe CHANGELOG.md como índice index_lines = [ "# Changelog\n", "\n", "Histórico particionado el 2026-05-19 por proyecto/intención. Cada\n", "archivo bajo `docs/changelog/` consolida las entradas de un\n", "subdirectorio del workspace.\n", "\n", ] for proj in sorted(buckets.keys()): n = len(buckets[proj]) index_lines.append(f"- [`{proj}`](docs/changelog/{proj}.md) — {n} entradas\n") CHANGELOG.write_text("".join(index_lines)) print(f"\nCHANGELOG.md reescrito como índice ({len(buckets)} proyectos)") if __name__ == "__main__": main()