#!/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())
