feat(ijfw-sync): rotina semanal de manutenção da memória (guardrail+higiene, minimalista) + systemd-user units
This commit is contained in:
Executable
+152
@@ -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())
|
||||
Reference in New Issue
Block a user