fifth-domain/zero-point/core-channel/revive-guard/navigation-memory-guard.py
2026-07-14 18:25:47 -07:00

80 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Reject orphaned persona-memory artifacts before push.
This guard is deliberately structural: it does not judge prose. It verifies
that a new/changed durable memory leaf has the minimum HLDP recovery fields
and can be reached from a current navigation page.
"""
import os, subprocess, sys
REQUIRED = ("trigger:", "emergence:", "lock:", "why:", "checkpoint")
NAVIGATION = ("INDEX.hdlp", "CURRENT.hdlp", "LL-CURRENT.hdlp", "BROADCAST-TOWER.hdlp")
def changed_files():
"""Return candidate files for a local check or a server receive range."""
if len(sys.argv) == 3 and sys.argv[1] == "--range":
old_rev, new_rev = sys.argv[2].split("..", 1)
if old_rev == "0" * 40:
command = ["git", "diff-tree", "--no-commit-id", "-r", "--name-only", "--diff-filter=ACM", new_rev]
else:
command = ["git", "diff", "--name-only", "--diff-filter=ACM", old_rev, new_rev]
result = subprocess.run(command, capture_output=True, text=True)
return sorted(x for x in result.stdout.splitlines() if x) if result.returncode == 0 else []
commands = [
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
]
files = set()
for cmd in commands:
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode == 0:
files.update(x for x in r.stdout.splitlines() if x)
return sorted(files)
def is_memory_leaf(path):
name = os.path.basename(path)
return name.startswith(("ZL-MEM-", "ZY-MEM-", "ZZ-MEM-")) or "/personas/" in path
def read(path):
with open(path, encoding="utf-8") as f:
return f.read()
def linked_from_navigation(path):
base = os.path.basename(path)
for root, _, names in os.walk("."):
if ".git" in root.split(os.sep):
continue
for name in names:
if name in NAVIGATION:
candidate = os.path.join(root, name)
try:
if base in read(candidate):
return candidate[2:] if candidate.startswith("./") else candidate
except OSError:
pass
return None
def main():
failures = []
for path in changed_files():
if not is_memory_leaf(path) or not os.path.isfile(path):
continue
content = read(path).lower()
missing = [field for field in REQUIRED if field not in content]
if missing:
failures.append((path, "缺永久记忆字段: " + ", ".join(missing)))
if not linked_from_navigation(path):
failures.append((path, "孤立记忆:未被 INDEX / CURRENT / LL-CURRENT / BROADCAST-TOWER 引用"))
if failures:
print("\n🧭 记忆—导航守门人:推送被暂停", file=sys.stderr)
print("持续记忆必须可恢复,而不是只被写进仓库。", file=sys.stderr)
for path, reason in failures:
print(f"{path}\n {reason}", file=sys.stderr)
print("修复:补齐 trigger/emergence/lock/why/checkpoint并在对应 INDEX 或 CURRENT 中建立路径。\n", file=sys.stderr)
return 1
print("✅ 记忆—导航守门人:没有发现孤立持续记忆")
return 0
if __name__ == "__main__":
sys.exit(main())