humanlang 3.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
human/__init__.py ADDED
@@ -0,0 +1,175 @@
1
+ import argparse
2
+ import fnmatch
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+ import tempfile
8
+ from functools import partial
9
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
10
+ from pathlib import Path
11
+
12
+ from . import cmd_map, decompiler
13
+
14
+ PKG = Path(__file__).parent
15
+
16
+ IGNORE_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv",
17
+ "build", "dist", ".idea", ".vscode"}
18
+ SUFFIXES = {".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".htm", ".css", ".md",
19
+ ".toml", ".json", ".yaml", ".yml", ".sh", ".rs", ".go", ".c", ".h",
20
+ ".cpp", ".java", ".rb", ".sql"}
21
+
22
+ DESTINATIONS = {
23
+ "claude": Path.home() / ".claude" / "skills",
24
+ "droid": Path.home() / ".factory" / "skills",
25
+ "shared": Path.home() / ".agents" / "skills",
26
+ }
27
+
28
+
29
+ def scan_files(root):
30
+ out = []
31
+ for dirpath, dirnames, filenames in os.walk(root):
32
+ d = Path(dirpath)
33
+ if d != root and (d / "human" / "human.json").is_file():
34
+ dirnames[:] = []
35
+ continue
36
+ dirnames[:] = sorted(x for x in dirnames
37
+ if not x.startswith(".") and x not in IGNORE_DIRS
38
+ and not (d == root and x == "human"))
39
+ for f in sorted(filenames):
40
+ if f.startswith(".") or f == "abstraction.txt":
41
+ continue
42
+ p = d / f
43
+ if p.suffix.lower() in SUFFIXES:
44
+ out.append(p.relative_to(root).as_posix())
45
+ return out
46
+
47
+
48
+ def is_ignored(rel, patterns):
49
+ for p in patterns:
50
+ q = (p["path"] if isinstance(p, dict) else p).rstrip("/")
51
+ if rel == q or rel.startswith(q + "/") or fnmatch.fnmatch(rel, q):
52
+ return True
53
+ return False
54
+
55
+
56
+ def cmd_init(a):
57
+ root = Path(a.folder).resolve()
58
+ root.mkdir(parents=True, exist_ok=True)
59
+ h = root / "human"
60
+ h.mkdir(exist_ok=True)
61
+ for name in ("web.html", "trees.js"):
62
+ shutil.copy(PKG / "reader" / name, h / name)
63
+ map_path = h / "human.json"
64
+ if map_path.exists():
65
+ data = json.loads(map_path.read_text())
66
+ decompiler.guard_structure(data, map_path)
67
+ else:
68
+ data = {"code_file": root.name, "explanations": [],
69
+ "not_covered": {"code_lines": [], "blank_lines": []}}
70
+ data["code_file"] = root.name
71
+ patterns = data.get("ignore", [])
72
+ found = scan_files(root)
73
+ data["ignore"] = patterns
74
+ data["files"] = [f for f in found if not is_ignored(f, patterns)]
75
+ data.pop("ignored", None)
76
+ map_path.write_text(json.dumps(data, indent=2) + "\n")
77
+ hidden = len(found) - len(data["files"])
78
+ tail = f", {hidden} ignored" if hidden else ""
79
+ print(f"project {root.name}: {len(data['files'])} files{tail}")
80
+ print(f"wrote {map_path}")
81
+ print(f"read it with: human serve (from {root})")
82
+
83
+
84
+ class FreshHandler(SimpleHTTPRequestHandler):
85
+ def end_headers(self):
86
+ self.send_header("Cache-Control", "no-cache")
87
+ super().end_headers()
88
+
89
+
90
+ def cmd_serve(a):
91
+ root = decompiler.find_root(Path(a.folder).resolve())
92
+ handler = partial(FreshHandler, directory=str(root))
93
+ srv = ThreadingHTTPServer(("0.0.0.0", a.port), handler)
94
+ print(f"serving {root}")
95
+ print(f"http://localhost:{a.port}/human/web.html")
96
+ try:
97
+ srv.serve_forever()
98
+ except KeyboardInterrupt:
99
+ pass
100
+
101
+
102
+ def cmd_skills(a):
103
+ dests = list(DESTINATIONS) if a.dest == "all" else [a.dest]
104
+ skills = sorted(p.name for p in (PKG / "skills").iterdir() if p.is_dir())
105
+ for dest in dests:
106
+ base = DESTINATIONS[dest]
107
+ base.mkdir(parents=True, exist_ok=True)
108
+ for name in skills:
109
+ dst = base / name
110
+ if dst.exists():
111
+ shutil.rmtree(dst)
112
+ shutil.copytree(PKG / "skills" / name, dst)
113
+ shutil.copytree(PKG / "shapes", dst / "shapes", dirs_exist_ok=True)
114
+ print(f"installed {name} -> {dst}")
115
+
116
+
117
+ def cmd_map_h(a):
118
+ if not a.verbatim:
119
+ cmd_map.cmd_map(a)
120
+ return
121
+ want = Path(a.verbatim).read_text().strip()
122
+ text = decompiler.read_text_arg(a)
123
+ got = decompiler.ANCHOR_RE.sub(lambda m: m.group(1), text)
124
+ if got != want:
125
+ sys.exit("with the pins stripped the text is not the abstraction word for word; "
126
+ "pins may go in, the words may not change")
127
+ fd, tmp = tempfile.mkstemp(suffix=".txt")
128
+ try:
129
+ os.write(fd, text.encode())
130
+ os.close(fd)
131
+ a.text = tmp
132
+ cmd_map.cmd_map(a)
133
+ finally:
134
+ os.unlink(tmp)
135
+
136
+
137
+ def main():
138
+ ap = argparse.ArgumentParser(prog="human")
139
+ sub = ap.add_subparsers(dest="cmd", required=True)
140
+ i = sub.add_parser("init")
141
+ i.add_argument("folder", nargs="?", default=".")
142
+ v = sub.add_parser("serve")
143
+ v.add_argument("folder", nargs="?", default=".")
144
+ v.add_argument("--port", type=int, default=8010)
145
+ k = sub.add_parser("skills")
146
+ k.add_argument("--dest", choices=list(DESTINATIONS) + ["all"], default="claude")
147
+ m = sub.add_parser("map")
148
+ m.add_argument("code_file")
149
+ m.add_argument("--block")
150
+ m.add_argument("--text")
151
+ m.add_argument("--verbatim")
152
+ r = sub.add_parser("retext")
153
+ r.add_argument("code_file")
154
+ r.add_argument("id", type=int)
155
+ r.add_argument("--text")
156
+ u = sub.add_parser("undo")
157
+ u.add_argument("code_file")
158
+ s = sub.add_parser("show")
159
+ s.add_argument("code_file")
160
+ l = sub.add_parser("lines")
161
+ l.add_argument("code_file")
162
+ y = sub.add_parser("sync")
163
+ y.add_argument("code_file")
164
+ y.add_argument("--old")
165
+ y.add_argument("--stale", type=int)
166
+ y.add_argument("--tries", type=int, default=4)
167
+ a = ap.parse_args()
168
+ {"init": cmd_init, "serve": cmd_serve, "skills": cmd_skills, "map": cmd_map_h,
169
+ "retext": decompiler.cmd_retext, "undo": decompiler.cmd_undo,
170
+ "show": decompiler.cmd_show, "lines": decompiler.cmd_lines,
171
+ "sync": decompiler.cmd_sync}[a.cmd](a)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
human/cmd_map.py ADDED
@@ -0,0 +1,78 @@
1
+ import json
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from . import decompiler
6
+
7
+
8
+ def load_map(map_path, name):
9
+ if map_path.exists():
10
+ data = json.loads(map_path.read_text())
11
+ decompiler.guard_structure(data, map_path)
12
+ return data
13
+ return {"code_file": name, "explanations": [], "not_covered": {"code_lines": [], "blank_lines": []}}
14
+
15
+
16
+ def entry_span(name, code_name, code_path, spans, n):
17
+ if name == code_name:
18
+ return [[1, n]]
19
+ if name in spans:
20
+ return [list(spans[name])]
21
+ sys.exit(f"{name!r} is not a block of {code_name}")
22
+
23
+
24
+ def next_id(data):
25
+ return max((e["id"] for e in data["explanations"]), default=0) + 1
26
+
27
+
28
+ def map_project(a, root):
29
+ if a.block:
30
+ sys.exit("a project map has no blocks of its own; drop --block")
31
+ map_path = root / "human" / "human.json"
32
+ data = load_map(map_path, root.name)
33
+ text = decompiler.read_text_arg(a)
34
+ eid = next_id(data)
35
+ try:
36
+ anchors = decompiler.build_anchors(text, data, {}, eid, root)
37
+ decompiler.project_pins(anchors)
38
+ except AssertionError as e:
39
+ sys.exit(str(e))
40
+ record = {"id": eid, "block": root.name, "block_lines": [],
41
+ "text": text, "anchors": anchors}
42
+ data["explanations"].append(record)
43
+ map_path.write_text(json.dumps(data, indent=2) + "\n")
44
+ files = sorted({x["file"] for x in anchors})
45
+ print(f"entry {eid}: {root.name}, {len(anchors)} pins into {len(files)} files ({', '.join(files)})")
46
+ print(f"wrote {map_path}")
47
+
48
+
49
+ def cmd_map(a):
50
+ code_path = Path(a.code_file).resolve()
51
+ root = decompiler.find_root(code_path)
52
+ if code_path.is_dir():
53
+ map_project(a, root)
54
+ return
55
+ code_name = decompiler.rel_name(code_path, root)
56
+ lines = code_path.read_text().splitlines()
57
+ spans = decompiler.block_spans(code_path, lines)
58
+ map_path = decompiler.map_path_of(code_path, root)
59
+ data = load_map(map_path, code_name)
60
+ text = decompiler.read_text_arg(a)
61
+ n = len(lines)
62
+ block = (a.block or code_name).strip()
63
+ block_lines = entry_span(block, code_name, code_path, spans, n)
64
+ eid = next_id(data)
65
+ try:
66
+ anchors = decompiler.build_anchors(text, data, spans, eid, root)
67
+ decompiler.check_cycle(data, eid, anchors)
68
+ except AssertionError as e:
69
+ sys.exit(str(e))
70
+ record = {"id": eid, "block": block, "block_lines": block_lines,
71
+ "text": text, "anchors": anchors}
72
+ data["explanations"].append(record)
73
+ missing, blank = decompiler.recompute(data, lines)
74
+ map_path.write_text(json.dumps(data, indent=2) + "\n")
75
+ decompiler.register_file(root, code_name)
76
+ print(f"entry {eid}: {block}, lines {decompiler.fmt(decompiler.expand(block_lines))}, {decompiler.anchor_counts(anchors)}")
77
+ decompiler.print_coverage(missing, blank, lines)
78
+ print(f"wrote {map_path}")