Compare commits

...
3 Commits
4 changed files with 160 additions and 0 deletions
+4
View File
@@ -14,3 +14,7 @@ __pycache__/
output/
downloads/
.DS_Store
# IJFW — working memory por-hook (Tier 1), nunca versionada
.ijfw/
ijfw/
+3
View File
@@ -0,0 +1,3 @@
# gerados pela rotina (regeneráveis)
.state.json
report.md
+152
View File
@@ -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())
+1
View File
@@ -2,3 +2,4 @@
okf-normalize-report.md
okf-rename-index.log
hub-okf-graph.dot
*.svg