From e8fb30f6b3de768e65ec653d7e344f62de491f18 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Mon, 29 Jun 2026 00:35:39 +0100 Subject: [PATCH] =?UTF-8?q?feat(ijfw-sync):=20rotina=20semanal=20de=20manu?= =?UTF-8?q?ten=C3=A7=C3=A3o=20da=20mem=C3=B3ria=20(guardrail+higiene,=20mi?= =?UTF-8?q?nimalista)=20+=20systemd-user=20units?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ijfw-sync/.gitignore | 3 + ijfw-sync/ijfw-sync.py | 152 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 ijfw-sync/.gitignore create mode 100755 ijfw-sync/ijfw-sync.py diff --git a/ijfw-sync/.gitignore b/ijfw-sync/.gitignore new file mode 100644 index 0000000..20bfc4c --- /dev/null +++ b/ijfw-sync/.gitignore @@ -0,0 +1,3 @@ +# gerados pela rotina (regeneráveis) +.state.json +report.md diff --git a/ijfw-sync/ijfw-sync.py b/ijfw-sync/ijfw-sync.py new file mode 100755 index 0000000..f35d490 --- /dev/null +++ b/ijfw-sync/ijfw-sync.py @@ -0,0 +1,152 @@ +#!/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())