feat(ijfw-sync): rotina semanal de manutenção da memória (guardrail+higiene, minimalista) + systemd-user units
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# gerados pela rotina (regeneráveis)
|
||||||
|
.state.json
|
||||||
|
report.md
|
||||||
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