refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reorganización del monorepo brahman (2026-05-19).
|
||||
|
||||
Pasos:
|
||||
1. git mv directorios según MOVES
|
||||
2. Renombra package names en Cargo.toml según PKG_RENAMES
|
||||
3. Reescribe paths relativos (path = "...") en TODOS los Cargo.toml
|
||||
4. Reescribe imports `use OLD_::` → `use NEW_::` en .rs
|
||||
5. Reescribe root Cargo.toml (members + workspace.dependencies)
|
||||
|
||||
Renames:
|
||||
shipote → shuma
|
||||
nouser → akasha
|
||||
yahweh → nahual (carpeta ui_engine también renombrada)
|
||||
lapaloma → pineal (promovida fuera de ui_engine)
|
||||
|
||||
Uso:
|
||||
python3 scripts/reorg.py --dry-run
|
||||
python3 scripts/reorg.py
|
||||
"""
|
||||
|
||||
import os, re, subprocess, sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path("/home/sergio/brahman")
|
||||
DRY = "--dry-run" in sys.argv
|
||||
|
||||
# ============================================================
|
||||
# 1) MOVES: old_canonical_dir → new_canonical_dir (relativo a ROOT)
|
||||
# ============================================================
|
||||
MOVES: dict[str, str] = {}
|
||||
|
||||
PROTOCOL = ["brahman-card", "brahman-card-wit", "brahman-cards",
|
||||
"brahman-handshake", "brahman-broker", "brahman-admin",
|
||||
"ente-card"]
|
||||
INIT_C = ["ente-zero", "ente-kernel", "ente-soma", "ente-snapshot"]
|
||||
RUNTIME = ["ente-bus", "ente-cas", "ente-wasm", "ente-brain", "ente-echo"]
|
||||
COMPAT = ["ente-logind-compat", "ente-hostnamed-compat", "ente-timedated-compat",
|
||||
"ente-localed-compat", "ente-journald-compat", "ente-resolved-compat",
|
||||
"ente-polkit-compat", "ente-machined-compat", "ente-systemd1-compat",
|
||||
"ente-notify-compat", "ente-timer-compat", "ente-tmpfiles-compat",
|
||||
"ente-binfmt-compat", "ente-policy-provider"]
|
||||
|
||||
for c in PROTOCOL: MOVES[f"crates/core/{c}"] = f"crates/protocol/{c}"
|
||||
for c in INIT_C: MOVES[f"crates/core/{c}"] = f"crates/init/{c}"
|
||||
for c in RUNTIME: MOVES[f"crates/core/{c}"] = f"crates/runtime/{c}"
|
||||
for c in COMPAT: MOVES[f"crates/core/{c}"] = f"crates/compat/{c}"
|
||||
|
||||
MOVES["crates/shared/brahman-sidecar"] = "crates/protocol/brahman-sidecar"
|
||||
MOVES["crates/shared/brahman-net"] = "crates/protocol/brahman-net"
|
||||
MOVES["crates/shared/ente-incarnate"] = "crates/init/ente-incarnate"
|
||||
|
||||
# shipote → shuma
|
||||
for c in ["card", "core", "protocol", "discern"]:
|
||||
MOVES[f"crates/modules/shipote/shipote-{c}"] = f"crates/modules/shuma/shuma-{c}"
|
||||
for c in ["cli", "daemon", "shell", "gateway"]:
|
||||
MOVES[f"crates/apps/shipote-{c}"] = f"crates/apps/shuma-{c}"
|
||||
|
||||
# nouser → akasha (mantiene naming interno sin prefijo)
|
||||
for c in ["card", "core", "nous", "nous-mock", "nous-real"]:
|
||||
MOVES[f"crates/modules/nouser/{c}"] = f"crates/modules/akasha/{c}"
|
||||
MOVES["crates/apps/nouser-explorer"] = "crates/apps/akasha-explorer"
|
||||
|
||||
# ui_engine → nahual (libs/* + widgets/* salvo lapaloma-*)
|
||||
NAHUAL_LIBS = ["core", "theme", "launcher", "bus", "meta-schema", "meta-runtime",
|
||||
"providers/fs", "providers/sqlite"]
|
||||
for c in NAHUAL_LIBS:
|
||||
MOVES[f"crates/modules/ui_engine/libs/{c}"] = f"crates/modules/nahual/libs/{c}"
|
||||
NAHUAL_WIDGETS = ["tree", "container_core", "splitter", "tabs", "tiled",
|
||||
"text_input", "meta-form", "banner", "card", "stat-card",
|
||||
"app-header", "theme-switcher"]
|
||||
for c in NAHUAL_WIDGETS:
|
||||
MOVES[f"crates/modules/ui_engine/widgets/{c}"] = f"crates/modules/nahual/widgets/{c}"
|
||||
|
||||
# lapaloma → pineal (promovida fuera de ui_engine)
|
||||
MOVES["crates/modules/ui_engine/libs/lapaloma-core"] = "crates/modules/pineal/core"
|
||||
PINEAL_W = ["render", "cartesian", "stream", "mesh", "financial",
|
||||
"polar", "heatmap", "treemap", "flow", "phosphor", "export"]
|
||||
for c in PINEAL_W:
|
||||
MOVES[f"crates/modules/ui_engine/widgets/lapaloma-{c}"] = f"crates/modules/pineal/{c}"
|
||||
MOVES["crates/modules/ui_engine/widgets/lapaloma"] = "crates/modules/pineal/umbrella"
|
||||
|
||||
# Apps: renames de yahweh, lapaloma
|
||||
MOVES["crates/apps/yahweh-shell"] = "crates/apps/nahual-shell"
|
||||
MOVES["crates/apps/file_explorer"] = "crates/apps/nahual-file-explorer"
|
||||
MOVES["crates/apps/database_explorer"] = "crates/apps/nahual-database-explorer"
|
||||
MOVES["crates/apps/text_viewer"] = "crates/apps/nahual-text-viewer"
|
||||
MOVES["crates/apps/image_viewer"] = "crates/apps/nahual-image-viewer"
|
||||
for c in ["demo", "stream-demo", "phosphor-demo", "financial-demo"]:
|
||||
MOVES[f"crates/apps/lapaloma-{c}"] = f"crates/apps/pineal-{c}"
|
||||
|
||||
# ============================================================
|
||||
# 2) PKG_RENAMES: nombre paquete viejo → nombre paquete nuevo
|
||||
# ============================================================
|
||||
PKG_RENAMES: dict[str, str] = {}
|
||||
|
||||
for s in ["card", "core", "protocol", "discern", "cli", "daemon", "shell", "gateway"]:
|
||||
PKG_RENAMES[f"shipote-{s}"] = f"shuma-{s}"
|
||||
|
||||
for s in ["card", "core", "nous", "nous-mock", "nous-real", "explorer"]:
|
||||
PKG_RENAMES[f"nouser-{s}"] = f"akasha-{s}"
|
||||
|
||||
YAHWEH = [
|
||||
"core", "theme", "launcher", "bus", "meta-schema", "meta-runtime",
|
||||
"provider-fs", "provider-sqlite",
|
||||
"widget-tree", "widget-container-core", "widget-splitter", "widget-tabs",
|
||||
"widget-tiled", "widget-text-input", "widget-meta-form", "widget-banner",
|
||||
"widget-card", "widget-stat-card", "widget-app-header",
|
||||
"widget-theme-switcher",
|
||||
"shell", "file-explorer", "database-explorer", "text-viewer", "image-viewer",
|
||||
]
|
||||
for s in YAHWEH: PKG_RENAMES[f"yahweh-{s}"] = f"nahual-{s}"
|
||||
|
||||
LAPAL = ["core", "render", "cartesian", "stream", "mesh", "financial",
|
||||
"polar", "heatmap", "treemap", "flow", "phosphor", "export",
|
||||
"demo", "stream-demo", "phosphor-demo", "financial-demo"]
|
||||
for s in LAPAL: PKG_RENAMES[f"lapaloma-{s}"] = f"pineal-{s}"
|
||||
PKG_RENAMES["lapaloma"] = "pineal"
|
||||
|
||||
# Bare names (binarios umbrella que se llaman como el proyecto)
|
||||
PKG_RENAMES["shipote"] = "shuma"
|
||||
PKG_RENAMES["yahweh"] = "nahual"
|
||||
PKG_RENAMES["nouser"] = "akasha"
|
||||
|
||||
# ============================================================
|
||||
# 3) Helpers
|
||||
# ============================================================
|
||||
|
||||
def sh(cmd, check=True):
|
||||
if DRY:
|
||||
print(f" DRY: {' '.join(cmd)}")
|
||||
return None
|
||||
return subprocess.run(cmd, check=check, cwd=str(ROOT))
|
||||
|
||||
def git_mv(src: str, dst: str):
|
||||
src_p = ROOT / src
|
||||
dst_p = ROOT / dst
|
||||
if not src_p.exists():
|
||||
print(f" SKIP {src} (no existe)")
|
||||
return
|
||||
if dst_p.exists():
|
||||
print(f" WARN {dst} ya existe — saltando")
|
||||
return
|
||||
dst_p.parent.mkdir(parents=True, exist_ok=True)
|
||||
sh(["git", "mv", src, dst])
|
||||
|
||||
def all_cargo_tomls() -> list[Path]:
|
||||
res = []
|
||||
for p in (ROOT / "crates").rglob("Cargo.toml"):
|
||||
if "target" in p.parts: continue
|
||||
res.append(p)
|
||||
return res
|
||||
|
||||
def all_rs() -> list[Path]:
|
||||
res = []
|
||||
for p in (ROOT / "crates").rglob("*.rs"):
|
||||
if "target" in p.parts: continue
|
||||
res.append(p)
|
||||
return res
|
||||
|
||||
# ============================================================
|
||||
# 4) Reescritura de paths relativos en Cargo.toml
|
||||
# ============================================================
|
||||
|
||||
# Después de los git mv, owner está en su NEW path. El path = "..."
|
||||
# dentro del file aún tiene el valor viejo (relativo al old owner).
|
||||
# Estrategia: probamos resolver el path desde el OLD owner; si el
|
||||
# target existe en MOVES o en disco, lo redirigimos al NEW target
|
||||
# y recomputamos relativo al NEW owner.
|
||||
|
||||
CURRENT_TO_OLD = {new: old for old, new in MOVES.items()}
|
||||
|
||||
def owner_old_for(toml: Path) -> str:
|
||||
cur = str(toml.parent.relative_to(ROOT))
|
||||
return CURRENT_TO_OLD.get(cur, cur)
|
||||
|
||||
def resolve_canon(base_dir: str, rel: str) -> str | None:
|
||||
try:
|
||||
abs_t = (ROOT / base_dir / rel).resolve()
|
||||
return str(abs_t.relative_to(ROOT))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def rewrite_paths_in_toml(toml: Path):
|
||||
text = toml.read_text()
|
||||
owner_old = owner_old_for(toml)
|
||||
owner_new = MOVES.get(owner_old, owner_old)
|
||||
|
||||
def repl(m: re.Match) -> str:
|
||||
rel_in_file = m.group(1)
|
||||
# Try resolve from owner_old first (file values are pre-move).
|
||||
# Si owner no se movió, owner_old == owner_new — sigue válido.
|
||||
for base in (owner_old, owner_new):
|
||||
tgt = resolve_canon(base, rel_in_file)
|
||||
if tgt is None:
|
||||
continue
|
||||
# ¿Conocemos un MOVES para este target?
|
||||
if tgt in MOVES:
|
||||
new_tgt = MOVES[tgt]
|
||||
elif (ROOT / tgt).exists():
|
||||
new_tgt = tgt
|
||||
else:
|
||||
continue
|
||||
new_rel = os.path.relpath(str(ROOT / new_tgt),
|
||||
str(ROOT / owner_new))
|
||||
return f'path = "{new_rel}"'
|
||||
return m.group(0)
|
||||
|
||||
new_text = re.sub(r'path\s*=\s*"([^"]+)"', repl, text)
|
||||
if new_text != text:
|
||||
if DRY:
|
||||
print(f" DRY paths: {toml.relative_to(ROOT)}")
|
||||
else:
|
||||
toml.write_text(new_text)
|
||||
|
||||
# ============================================================
|
||||
# 5) Reescritura de package names en Cargo.toml
|
||||
# ============================================================
|
||||
|
||||
def rewrite_pkg_names_in_toml(toml: Path):
|
||||
text = toml.read_text()
|
||||
orig = text
|
||||
# Ordenamos de más largo a más corto para evitar que `shipote` reemplace
|
||||
# antes que `shipote-core` cuando este último también está en el map.
|
||||
for old, new in sorted(PKG_RENAMES.items(), key=lambda kv: -len(kv[0])):
|
||||
# name = "OLD" / package = "OLD"
|
||||
text = re.sub(rf'(\bname\s*=\s*)"{re.escape(old)}"',
|
||||
rf'\1"{new}"', text)
|
||||
text = re.sub(rf'(\bpackage\s*=\s*)"{re.escape(old)}"',
|
||||
rf'\1"{new}"', text)
|
||||
# Dep key al inicio de línea: OLD = ...
|
||||
text = re.sub(rf'^([ \t]*){re.escape(old)}(\s*=)',
|
||||
rf'\1{new}\2', text, flags=re.MULTILINE)
|
||||
# Features y strings: "dep:OLD" → "dep:NEW"
|
||||
text = re.sub(rf'"dep:{re.escape(old)}"', f'"dep:{new}"', text)
|
||||
# Features: "OLD/feature" → "NEW/feature"
|
||||
text = re.sub(rf'"{re.escape(old)}/', f'"{new}/', text)
|
||||
# Features: "OLD" → "NEW" (referencia bare en feature list)
|
||||
text = re.sub(rf'"{re.escape(old)}"', f'"{new}"', text)
|
||||
if text != orig:
|
||||
if DRY:
|
||||
print(f" DRY names: {toml.relative_to(ROOT)}")
|
||||
else:
|
||||
toml.write_text(text)
|
||||
|
||||
# ============================================================
|
||||
# 6) Reescritura de imports en .rs
|
||||
# ============================================================
|
||||
|
||||
USE_RENAMES = {k.replace("-", "_"): v.replace("-", "_")
|
||||
for k, v in PKG_RENAMES.items()}
|
||||
|
||||
def rewrite_imports_in_rs(rs: Path):
|
||||
text = rs.read_text()
|
||||
orig = text
|
||||
for old, new in USE_RENAMES.items():
|
||||
text = re.sub(rf'(?<![A-Za-z0-9_]){re.escape(old)}(?![A-Za-z0-9_])',
|
||||
new, text)
|
||||
if text != orig:
|
||||
if DRY:
|
||||
print(f" DRY imports: {rs.relative_to(ROOT)}")
|
||||
else:
|
||||
rs.write_text(text)
|
||||
|
||||
# ============================================================
|
||||
# 7) Workspace root Cargo.toml
|
||||
# ============================================================
|
||||
|
||||
def rewrite_workspace_toml():
|
||||
p = ROOT / "Cargo.toml"
|
||||
text = p.read_text()
|
||||
orig = text
|
||||
for old, new in MOVES.items():
|
||||
text = text.replace(f'"{old}"', f'"{new}"')
|
||||
for old, new in PKG_RENAMES.items():
|
||||
text = re.sub(rf'^([ \t]*){re.escape(old)}(\s*=)',
|
||||
rf'\1{new}\2', text, flags=re.MULTILINE)
|
||||
if text != orig:
|
||||
if DRY:
|
||||
print(f" DRY workspace toml")
|
||||
else:
|
||||
p.write_text(text)
|
||||
|
||||
# ============================================================
|
||||
# Driver
|
||||
# ============================================================
|
||||
|
||||
def run():
|
||||
print(f"=== reorg.py (DRY={DRY}) ===")
|
||||
print(f" {len(MOVES)} moves, {len(PKG_RENAMES)} pkg renames")
|
||||
|
||||
print("\n-- step 1: git mv --")
|
||||
for old, new in MOVES.items():
|
||||
git_mv(old, new)
|
||||
|
||||
print("\n-- step 2: package names --")
|
||||
for toml in all_cargo_tomls():
|
||||
rewrite_pkg_names_in_toml(toml)
|
||||
|
||||
print("\n-- step 3: paths relativos --")
|
||||
for toml in all_cargo_tomls():
|
||||
rewrite_paths_in_toml(toml)
|
||||
|
||||
print("\n-- step 4: imports en .rs --")
|
||||
for rs in all_rs():
|
||||
rewrite_imports_in_rs(rs)
|
||||
|
||||
print("\n-- step 5: workspace Cargo.toml --")
|
||||
rewrite_workspace_toml()
|
||||
|
||||
print("\nOK")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Particiona CHANGELOG.md raíz por proyecto/intención.
|
||||
|
||||
Cada sección `### tipo(componente):` se rutea a `docs/changelog/<proyecto>.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()
|
||||
Reference in New Issue
Block a user