#!/usr/bin/env python3 """ ijfw-sync — rotina de manutenção da memória IJFW (minimalista). Faz GUARDRAIL + HIGIENE, NÃO auto-cura (a curação de factos precisa de um LLM e fica agent-assisted quando a flag surge no prelude): 1. Detecta MEMORY.md manuais novos/alterados desde a última corrida (state por sha256). MEMORY.md está deprecated — qualquer novo/alterado é matéria a rever. 2. Higiene: conta MEMORY.md no vault + factos na KB central; reporta drift. 3. Reporta em camadas (silent-on-success): - journald (stdout) + report.md (sempre, 1 linha datada); - observation na memória IJFW (via stdio JSON-RPC ao server.js) SÓ quando há novos/alterados, para a flag surgir no prelude da próxima sessão. Não toca no Desk nem em push. Vive em Dev (regra 48: scripts fora do vault). Uso: python3 ijfw-sync.py [--dry-run] """ import os, sys, json, hashlib, subprocess, datetime from pathlib import Path ROOTS = [Path("/media/ealmeida/Dados/Hub"), Path("/media/ealmeida/Dados/Dev")] EXCLUDE_DIRS = {".git","node_modules",".stversions",".obsidian",".ijfw","ijfw",".trash", ".cache",".npm",".nvm","99-Arquivo",".claude",".gemini",".config", "venv","__pycache__",".vscode",".cursor",".gstack","_templates"} HERE = Path(__file__).resolve().parent STATE = HERE / ".state.json" REPORT = HERE / "report.md" SERVER = Path.home() / ".ijfw/mcp-server/src/server.js" def _node(): import shutil, glob n = shutil.which("node") if n: return n for pat in ["/opt/homebrew/bin/node","/usr/local/bin/node","/usr/bin/node", str(Path.home())+"/.nvm/versions/node/*/bin/node"]: for c in sorted(glob.glob(pat), reverse=True): if os.access(c, os.X_OK): return c return "node" NODE = _node() MEMORY_PROJECT = "/media/ealmeida/Dados/Hub/04-Stack/02.04-Sistemas/MemoriaCentral" CENTRAL_KB = Path(MEMORY_PROJECT) / "ijfw/memory/knowledge.md" DRY = "--dry-run" in sys.argv def find_memory_files(): out = [] for root in ROOTS: if not root.exists(): continue for dirpath, dirs, files in os.walk(root): dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")] for f in files: # MEMORY.md manuais (exclui variantes geridas já fora por .claude/.ijfw) if f == "MEMORY.md" or (f.startswith("MEMORY-") and f.endswith(".md")): out.append(Path(dirpath) / f) return out def sha(p): try: return hashlib.sha256(p.read_bytes()).hexdigest()[:16] except Exception: return "?" def store_observation(summary, content, tags): """Guarda uma observation na memória via stdio JSON-RPC ao server.js (cwd = projecto memória).""" reqs = [ {"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2024-11-05","capabilities":{}, "clientInfo":{"name":"ijfw-sync","version":"1.0"}}}, {"jsonrpc":"2.0","method":"notifications/initialized"}, {"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"ijfw_memory_store", "arguments":{"type":"observation","summary":summary[:80], "content":content,"tags":tags}}}, ] inp = "\n".join(json.dumps(r) for r in reqs) + "\n" try: r = subprocess.run([NODE, str(SERVER)], input=inp, capture_output=True, text=True, timeout=60, cwd=MEMORY_PROJECT) return "Stored observation" in r.stdout except Exception as e: print(f" ! store falhou: {e}", file=sys.stderr) return False def main(): now = datetime.datetime.now().astimezone().isoformat(timespec="seconds") state = {} if STATE.exists(): try: state = json.loads(STATE.read_text()) except Exception: state = {} files = find_memory_files() cur = {str(p): sha(p) for p in files} first_run = not state new = [p for p in cur if p not in state] changed = [p for p in cur if p in state and state[p] != cur[p]] total = len(cur) if first_run: # 1ª corrida = estabelecer baseline (os MEMORY.md actuais já são conhecidos/migrados); # não sinalizar todos como novos. new, changed = [], [] print(f"[ijfw-sync] baseline inicial: {total} MEMORY.md registados (sem flag).") kb_facts = 0 if CENTRAL_KB.exists(): kb_facts = CENTRAL_KB.read_text(encoding="utf-8", errors="replace").count("\n### ") \ + CENTRAL_KB.read_text(encoding="utf-8", errors="replace").count("\ntype:") # --- report: journald (stdout) sempre --- flagged = len(new) + len(changed) print(f"[ijfw-sync {now}] MEMORY.md total={total} novos={len(new)} alterados={len(changed)} | KB central ~{kb_facts} factos") if new: for p in new: print(f" + NOVO {p}") if changed: for p in changed: print(f" ~ ALTERADO {p}") # --- report.md (sempre, 1 linha datada) --- line = f"- {now} · total={total} · novos={len(new)} · alterados={len(changed)} · KB~{kb_facts}" if flagged: line += " · **REVER** → " + ", ".join(os.path.basename(os.path.dirname(p)) + "/MEMORY.md" for p in (new+changed)[:8]) if not DRY: hdr = "" if REPORT.exists() else "# ijfw-sync — log de manutenção da memória\n\n" with REPORT.open("a", encoding="utf-8") as fh: fh.write(hdr + line + "\n") # --- observation só quando há matéria (flag no prelude) --- if flagged and not DRY: lst = "; ".join(p.replace("/media/ealmeida/Dados/","") for p in (new+changed)[:10]) ok = store_observation( summary=f"ijfw-sync: {flagged} MEMORY.md novos/alterados a rever ({now[:10]})", content=(f"A rotina ijfw-sync detectou {len(new)} novos e {len(changed)} alterados " f"MEMORY.md (deprecated). Rever e curar para a memória IJFW via fan-out: {lst}. " f"MEMORY.md não devia ser criado — verificar se é violação da política."), tags=["ijfw-sync","manutencao","memory-md","guardrail"]) print(f" → observation guardada na memória: {ok}") # --- persist state --- if not DRY: STATE.write_text(json.dumps(cur, indent=0, ensure_ascii=False)) else: print(" [DRY-RUN] state e report.md não escritos") return 0 if __name__ == "__main__": sys.exit(main())