Publish REPO-001 through REPO-008 as domestic routes, add the public AI discovery API, and demote Singapore paths to historical backup.
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
#!/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", "--root", "--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())
|