refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
1#!/usr/bin/env python3
2"""
3Reorganización del monorepo brahman (2026-05-19).
4
5Pasos:
6 1. git mv directorios según MOVES
7 2. Renombra package names en Cargo.toml según PKG_RENAMES
8 3. Reescribe paths relativos (path = "...") en TODOS los Cargo.toml
9 4. Reescribe imports `use OLD_::` → `use NEW_::` en .rs
10 5. Reescribe root Cargo.toml (members + workspace.dependencies)
11
12Renames:
13 shipote → shuma
14 nouser → akasha
15 yahweh → nahual (carpeta ui_engine también renombrada)
16 lapaloma → pineal (promovida fuera de ui_engine)
17
18Uso:
19 python3 scripts/reorg.py --dry-run
20 python3 scripts/reorg.py
21"""
22
23import os, re, subprocess, sys
24from pathlib import Path
25
26ROOT = Path("/home/sergio/brahman")
27DRY = "--dry-run" in sys.argv
28
29# ============================================================
30# 1) MOVES: old_canonical_dir → new_canonical_dir (relativo a ROOT)
31# ============================================================
32MOVES: dict[str, str] = {}
33
34PROTOCOL = ["brahman-card", "brahman-card-wit", "brahman-cards",
35 "brahman-handshake", "brahman-broker", "brahman-admin",
36 "ente-card"]
37INIT_C = ["ente-zero", "ente-kernel", "ente-soma", "ente-snapshot"]
38RUNTIME = ["ente-bus", "ente-cas", "ente-wasm", "ente-brain", "ente-echo"]
39COMPAT = ["ente-logind-compat", "ente-hostnamed-compat", "ente-timedated-compat",
40 "ente-localed-compat", "ente-journald-compat", "ente-resolved-compat",
41 "ente-polkit-compat", "ente-machined-compat", "ente-systemd1-compat",
42 "ente-notify-compat", "ente-timer-compat", "ente-tmpfiles-compat",
43 "ente-binfmt-compat", "ente-policy-provider"]
44
45for c in PROTOCOL: MOVES[f"crates/core/{c}"] = f"crates/protocol/{c}"
46for c in INIT_C: MOVES[f"crates/core/{c}"] = f"crates/init/{c}"
47for c in RUNTIME: MOVES[f"crates/core/{c}"] = f"crates/runtime/{c}"
48for c in COMPAT: MOVES[f"crates/core/{c}"] = f"crates/compat/{c}"
49
50MOVES["crates/shared/brahman-sidecar"] = "crates/protocol/brahman-sidecar"
51MOVES["crates/shared/brahman-net"] = "crates/protocol/brahman-net"
52MOVES["crates/shared/ente-incarnate"] = "crates/init/ente-incarnate"
53
54# shipote → shuma
55for c in ["card", "core", "protocol", "discern"]:
56 MOVES[f"crates/modules/shipote/shipote-{c}"] = f"crates/modules/shuma/shuma-{c}"
57for c in ["cli", "daemon", "shell", "gateway"]:
58 MOVES[f"crates/apps/shipote-{c}"] = f"crates/apps/shuma-{c}"
59
60# nouser → akasha (mantiene naming interno sin prefijo)
61for c in ["card", "core", "nous", "nous-mock", "nous-real"]:
62 MOVES[f"crates/modules/nouser/{c}"] = f"crates/modules/akasha/{c}"
63MOVES["crates/apps/nouser-explorer"] = "crates/apps/akasha-explorer"
64
65# ui_engine → nahual (libs/* + widgets/* salvo lapaloma-*)
66NAHUAL_LIBS = ["core", "theme", "launcher", "bus", "meta-schema", "meta-runtime",
67 "providers/fs", "providers/sqlite"]
68for c in NAHUAL_LIBS:
69 MOVES[f"crates/modules/ui_engine/libs/{c}"] = f"crates/modules/nahual/libs/{c}"
70NAHUAL_WIDGETS = ["tree", "container_core", "splitter", "tabs", "tiled",
71 "text_input", "meta-form", "banner", "card", "stat-card",
72 "app-header", "theme-switcher"]
73for c in NAHUAL_WIDGETS:
74 MOVES[f"crates/modules/ui_engine/widgets/{c}"] = f"crates/modules/nahual/widgets/{c}"
75
76# lapaloma → pineal (promovida fuera de ui_engine)
77MOVES["crates/modules/ui_engine/libs/lapaloma-core"] = "crates/modules/pineal/core"
78PINEAL_W = ["render", "cartesian", "stream", "mesh", "financial",
79 "polar", "heatmap", "treemap", "flow", "phosphor", "export"]
80for c in PINEAL_W:
81 MOVES[f"crates/modules/ui_engine/widgets/lapaloma-{c}"] = f"crates/modules/pineal/{c}"
82MOVES["crates/modules/ui_engine/widgets/lapaloma"] = "crates/modules/pineal/umbrella"
83
84# Apps: renames de yahweh, lapaloma
85MOVES["crates/apps/yahweh-shell"] = "crates/apps/nahual-shell"
86MOVES["crates/apps/file_explorer"] = "crates/apps/nahual-file-explorer"
87MOVES["crates/apps/database_explorer"] = "crates/apps/nahual-database-explorer"
88MOVES["crates/apps/text_viewer"] = "crates/apps/nahual-text-viewer"
89MOVES["crates/apps/image_viewer"] = "crates/apps/nahual-image-viewer"
90for c in ["demo", "stream-demo", "phosphor-demo", "financial-demo"]:
91 MOVES[f"crates/apps/lapaloma-{c}"] = f"crates/apps/pineal-{c}"
92
93# ============================================================
94# 2) PKG_RENAMES: nombre paquete viejo → nombre paquete nuevo
95# ============================================================
96PKG_RENAMES: dict[str, str] = {}
97
98for s in ["card", "core", "protocol", "discern", "cli", "daemon", "shell", "gateway"]:
99 PKG_RENAMES[f"shipote-{s}"] = f"shuma-{s}"
100
101for s in ["card", "core", "nous", "nous-mock", "nous-real", "explorer"]:
102 PKG_RENAMES[f"nouser-{s}"] = f"akasha-{s}"
103
104YAHWEH = [
105 "core", "theme", "launcher", "bus", "meta-schema", "meta-runtime",
106 "provider-fs", "provider-sqlite",
107 "widget-tree", "widget-container-core", "widget-splitter", "widget-tabs",
108 "widget-tiled", "widget-text-input", "widget-meta-form", "widget-banner",
109 "widget-card", "widget-stat-card", "widget-app-header",
110 "widget-theme-switcher",
111 "shell", "file-explorer", "database-explorer", "text-viewer", "image-viewer",
112]
113for s in YAHWEH: PKG_RENAMES[f"yahweh-{s}"] = f"nahual-{s}"
114
115LAPAL = ["core", "render", "cartesian", "stream", "mesh", "financial",
116 "polar", "heatmap", "treemap", "flow", "phosphor", "export",
117 "demo", "stream-demo", "phosphor-demo", "financial-demo"]
118for s in LAPAL: PKG_RENAMES[f"lapaloma-{s}"] = f"pineal-{s}"
119PKG_RENAMES["lapaloma"] = "pineal"
120
121# Bare names (binarios umbrella que se llaman como el proyecto)
122PKG_RENAMES["shipote"] = "shuma"
123PKG_RENAMES["yahweh"] = "nahual"
124PKG_RENAMES["nouser"] = "akasha"
125
126# ============================================================
127# 3) Helpers
128# ============================================================
129
130def sh(cmd, check=True):
131 if DRY:
132 print(f" DRY: {' '.join(cmd)}")
133 return None
134 return subprocess.run(cmd, check=check, cwd=str(ROOT))
135
136def git_mv(src: str, dst: str):
137 src_p = ROOT / src
138 dst_p = ROOT / dst
139 if not src_p.exists():
140 print(f" SKIP {src} (no existe)")
141 return
142 if dst_p.exists():
143 print(f" WARN {dst} ya existe — saltando")
144 return
145 dst_p.parent.mkdir(parents=True, exist_ok=True)
146 sh(["git", "mv", src, dst])
147
148def all_cargo_tomls() -> list[Path]:
149 res = []
150 for p in (ROOT / "crates").rglob("Cargo.toml"):
151 if "target" in p.parts: continue
152 res.append(p)
153 return res
154
155def all_rs() -> list[Path]:
156 res = []
157 for p in (ROOT / "crates").rglob("*.rs"):
158 if "target" in p.parts: continue
159 res.append(p)
160 return res
161
162# ============================================================
163# 4) Reescritura de paths relativos en Cargo.toml
164# ============================================================
165
166# Después de los git mv, owner está en su NEW path. El path = "..."
167# dentro del file aún tiene el valor viejo (relativo al old owner).
168# Estrategia: probamos resolver el path desde el OLD owner; si el
169# target existe en MOVES o en disco, lo redirigimos al NEW target
170# y recomputamos relativo al NEW owner.
171
172CURRENT_TO_OLD = {new: old for old, new in MOVES.items()}
173
174def owner_old_for(toml: Path) -> str:
175 cur = str(toml.parent.relative_to(ROOT))
176 return CURRENT_TO_OLD.get(cur, cur)
177
178def resolve_canon(base_dir: str, rel: str) -> str | None:
179 try:
180 abs_t = (ROOT / base_dir / rel).resolve()
181 return str(abs_t.relative_to(ROOT))
182 except ValueError:
183 return None
184
185def rewrite_paths_in_toml(toml: Path):
186 text = toml.read_text()
187 owner_old = owner_old_for(toml)
188 owner_new = MOVES.get(owner_old, owner_old)
189
190 def repl(m: re.Match) -> str:
191 rel_in_file = m.group(1)
192 # Try resolve from owner_old first (file values are pre-move).
193 # Si owner no se movió, owner_old == owner_new — sigue válido.
194 for base in (owner_old, owner_new):
195 tgt = resolve_canon(base, rel_in_file)
196 if tgt is None:
197 continue
198 # ¿Conocemos un MOVES para este target?
199 if tgt in MOVES:
200 new_tgt = MOVES[tgt]
201 elif (ROOT / tgt).exists():
202 new_tgt = tgt
203 else:
204 continue
205 new_rel = os.path.relpath(str(ROOT / new_tgt),
206 str(ROOT / owner_new))
207 return f'path = "{new_rel}"'
208 return m.group(0)
209
210 new_text = re.sub(r'path\s*=\s*"([^"]+)"', repl, text)
211 if new_text != text:
212 if DRY:
213 print(f" DRY paths: {toml.relative_to(ROOT)}")
214 else:
215 toml.write_text(new_text)
216
217# ============================================================
218# 5) Reescritura de package names en Cargo.toml
219# ============================================================
220
221def rewrite_pkg_names_in_toml(toml: Path):
222 text = toml.read_text()
223 orig = text
224 # Ordenamos de más largo a más corto para evitar que `shipote` reemplace
225 # antes que `shipote-core` cuando este último también está en el map.
226 for old, new in sorted(PKG_RENAMES.items(), key=lambda kv: -len(kv[0])):
227 # name = "OLD" / package = "OLD"
228 text = re.sub(rf'(\bname\s*=\s*)"{re.escape(old)}"',
229 rf'\1"{new}"', text)
230 text = re.sub(rf'(\bpackage\s*=\s*)"{re.escape(old)}"',
231 rf'\1"{new}"', text)
232 # Dep key al inicio de línea: OLD = ...
233 text = re.sub(rf'^([ \t]*){re.escape(old)}(\s*=)',
234 rf'\1{new}\2', text, flags=re.MULTILINE)
235 # Features y strings: "dep:OLD" → "dep:NEW"
236 text = re.sub(rf'"dep:{re.escape(old)}"', f'"dep:{new}"', text)
237 # Features: "OLD/feature" → "NEW/feature"
238 text = re.sub(rf'"{re.escape(old)}/', f'"{new}/', text)
239 # Features: "OLD" → "NEW" (referencia bare en feature list)
240 text = re.sub(rf'"{re.escape(old)}"', f'"{new}"', text)
241 if text != orig:
242 if DRY:
243 print(f" DRY names: {toml.relative_to(ROOT)}")
244 else:
245 toml.write_text(text)
246
247# ============================================================
248# 6) Reescritura de imports en .rs
249# ============================================================
250
251USE_RENAMES = {k.replace("-", "_"): v.replace("-", "_")
252 for k, v in PKG_RENAMES.items()}
253
254def rewrite_imports_in_rs(rs: Path):
255 text = rs.read_text()
256 orig = text
257 for old, new in USE_RENAMES.items():
258 text = re.sub(rf'(?<![A-Za-z0-9_]){re.escape(old)}(?![A-Za-z0-9_])',
259 new, text)
260 if text != orig:
261 if DRY:
262 print(f" DRY imports: {rs.relative_to(ROOT)}")
263 else:
264 rs.write_text(text)
265
266# ============================================================
267# 7) Workspace root Cargo.toml
268# ============================================================
269
270def rewrite_workspace_toml():
271 p = ROOT / "Cargo.toml"
272 text = p.read_text()
273 orig = text
274 for old, new in MOVES.items():
275 text = text.replace(f'"{old}"', f'"{new}"')
276 for old, new in PKG_RENAMES.items():
277 text = re.sub(rf'^([ \t]*){re.escape(old)}(\s*=)',
278 rf'\1{new}\2', text, flags=re.MULTILINE)
279 if text != orig:
280 if DRY:
281 print(f" DRY workspace toml")
282 else:
283 p.write_text(text)
284
285# ============================================================
286# Driver
287# ============================================================
288
289def run():
290 print(f"=== reorg.py (DRY={DRY}) ===")
291 print(f" {len(MOVES)} moves, {len(PKG_RENAMES)} pkg renames")
292
293 print("\n-- step 1: git mv --")
294 for old, new in MOVES.items():
295 git_mv(old, new)
296
297 print("\n-- step 2: package names --")
298 for toml in all_cargo_tomls():
299 rewrite_pkg_names_in_toml(toml)
300
301 print("\n-- step 3: paths relativos --")
302 for toml in all_cargo_tomls():
303 rewrite_paths_in_toml(toml)
304
305 print("\n-- step 4: imports en .rs --")
306 for rs in all_rs():
307 rewrite_imports_in_rs(rs)
308
309 print("\n-- step 5: workspace Cargo.toml --")
310 rewrite_workspace_toml()
311
312 print("\nOK")
313
314if __name__ == "__main__":
315 run()