mapname.py

#!/usr/bin/env python3
"""mapname.py — Beth's Library map-file tool (v2 dialect, hex-armored names).

Three commands, no dependencies beyond Python 3.8+:

  python3 mapname.py new  "cw≅crt∵邻接丢失" --seq 0014 --map-dir ./map
      Creates a properly named .txt map file: NFC-normalizes the dialect
      string, hex-armors it into the filename, stamps the time itself
      (models never supply timestamps), writes the shebang + dialect body,
      and prints the path. Add provenance with --at "s2026-09-04 t041 L112-118".

  python3 mapname.py read ./map            (or a single file path)
      The decoder ring in action: prints every map file as
      seq | timestamp | decoded dialect line. This is the "really want
      to see it" gate — one deliberate command renders the layer legible.

  python3 mapname.py check ./map
      The validator, v0: grammar regex, hex decodes cleanly, NFC holds,
      body shebang present, body dialect matches the filename payload,
      seq uniqueness. Exit code 1 if anything fails. Findings, not fixes.

Filename grammar (all ASCII, safe on Windows/mac/Linux, sorts by seq):
  SEQ _._ TIMESTAMP _._ x--HEXPAYLOAD .txt
  SEQ: digits, insertable with hyphen segments (0014, 0014-50, 0014-50-50)
  TIMESTAMP: YYYYMMDDHHMMSS, machine-stamped
  x--: dialect marker (punycode homage): "the rest is encoded"
  HEXPAYLOAD: lowercase hex of NFC UTF-8 dialect string

Body format:
  line 1: ⟦map:v2:zh⟧          (the shebang: put on your hex pants)
  line 2: the dialect string in native UTF-8 (models read this raw)
  line 3: @ provenance pointer(s), optional (@ session turn lines)
"""

import argparse
import re
import sys
import unicodedata
from datetime import datetime
from pathlib import Path

SHEBANG = "\u27e6map:v2:zh\u27e7"  # ⟦map:v2:zh⟧
NAME_RE = re.compile(
    r"^(?P<seq>\d{4}(?:-\d{2})*)_\._"
    r"(?P<ts>\d{14})_\._"
    r"x--(?P<hex>[0-9a-f]+)\.txt$"
)


def nfc(s: str) -> str:
    return unicodedata.normalize("NFC", s)


def armor(dialect: str) -> str:
    return nfc(dialect).encode("utf-8").hex()


def unarmor(hexpayload: str) -> str:
    return bytes.fromhex(hexpayload).decode("utf-8")


def cmd_new(args) -> int:
    dialect = nfc(args.dialect)
    ts = datetime.now().strftime("%Y%m%d%H%M%S")
    hexpayload = armor(dialect)
    fname = f"{args.seq}_._{ts}_._x--{hexpayload}.txt"
    if len(fname) > 240:
        print(f"REFUSED: filename would be {len(fname)} chars (>240).", file=sys.stderr)
        print("Shorten the mechanism; detail belongs in the body.", file=sys.stderr)
        return 1
    map_dir = Path(args.map_dir)
    map_dir.mkdir(parents=True, exist_ok=True)
    for existing in map_dir.glob("*.txt"):
        m = NAME_RE.match(existing.name)
        if m and m.group("seq") == args.seq:
            print(f"REFUSED: seq {args.seq} already exists: {existing.name}", file=sys.stderr)
            return 1
    path = map_dir / fname
    body_lines = [SHEBANG, dialect]
    if args.at:
        body_lines.append(f"@ {args.at}")
    path.write_text("\n".join(body_lines) + "\n", encoding="utf-8")
    print(path)
    return 0


def iter_map_files(target: Path):
    if target.is_file():
        yield target
        return

    def seq_key(p: Path):
        m = NAME_RE.match(p.name)
        if not m:
            return (1, p.name)  # non-map files sort last, by name
        return (0, tuple(int(s) for s in m.group("seq").split("-")))

    yield from sorted(target.glob("*.txt"), key=seq_key)


def cmd_read(args) -> int:
    target = Path(args.path)
    for path in iter_map_files(target):
        m = NAME_RE.match(path.name)
        if not m:
            print(f"{path.name}  |  (not a map file)")
            continue
        try:
            dialect = unarmor(m.group("hex"))
        except (ValueError, UnicodeDecodeError):
            dialect = "(hex does not decode)"
        print(f"{m.group('seq'):<12} | {m.group('ts')} | {dialect}")
    return 0


def cmd_check(args) -> int:
    target = Path(args.path)
    findings = []
    seqs = {}
    for path in iter_map_files(target):
        name = path.name
        m = NAME_RE.match(name)
        if not m:
            findings.append(f"{name}: fails filename grammar")
            continue
        seq = m.group("seq")
        if seq in seqs:
            findings.append(f"{name}: seq {seq} duplicates {seqs[seq]}")
        else:
            seqs[seq] = name
        try:
            decoded = unarmor(m.group("hex"))
        except (ValueError, UnicodeDecodeError):
            findings.append(f"{name}: hex payload does not decode to UTF-8")
            continue
        if decoded != nfc(decoded):
            findings.append(f"{name}: payload is not NFC-normalized")
        try:
            lines = path.read_text(encoding="utf-8").splitlines()
        except UnicodeDecodeError:
            findings.append(f"{name}: body is not valid UTF-8")
            continue
        if not lines or lines[0] != SHEBANG:
            findings.append(f"{name}: body missing shebang {SHEBANG}")
            continue
        if len(lines) < 2 or nfc(lines[1]) != decoded:
            findings.append(f"{name}: body dialect line does not match filename payload")
    if findings:
        print(f"{len(findings)} finding(s):")
        for f in findings:
            print(f"  - {f}")
        return 1
    print("clean: all map files pass grammar, decode, NFC, shebang, body-match, seq-unique")
    return 0


def main() -> int:
    p = argparse.ArgumentParser(description="Map-file tool: new / read / check")
    sub = p.add_subparsers(dest="cmd", required=True)

    pn = sub.add_parser("new", help="create a map file from a dialect string")
    pn.add_argument("dialect", help='e.g. "cw≅crt∵邻接丢失"')
    pn.add_argument("--seq", required=True, help="e.g. 0014 or 0014-50")
    pn.add_argument("--map-dir", default="./map")
    pn.add_argument("--at", default=None, help='provenance, e.g. "s2026-09-04 t041 L112-118"')
    pn.set_defaults(fn=cmd_new)

    pr = sub.add_parser("read", help="decode filenames back to dialect lines")
    pr.add_argument("path", nargs="?", default="./map")
    pr.set_defaults(fn=cmd_read)

    pc = sub.add_parser("check", help="validate grammar, decode, NFC, shebang, seq")
    pc.add_argument("path", nargs="?", default="./map")
    pc.set_defaults(fn=cmd_check)

    args = p.parse_args()
    return args.fn(args)


if __name__ == "__main__":
    sys.exit(main())