adds skeleton function CLIs and better tree_dict pretty print
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 19 Nov 2025 05:51:20 +0000 (05:51 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 19 Nov 2025 05:51:20 +0000 (05:51 +0000)
tool/skeleton/A_minus_B [new file with mode: 0755]
tool/skeleton/Harmony_where [new file with mode: 0755]
tool/skeleton/in_between_and_below [new file with mode: 0755]
tool/skeleton/make_Harmony_tree_dict [new file with mode: 0755]
tool/skeleton/meta.py
tool/skeleton/newer [new file with mode: 0755]
tool/skeleton/older [new file with mode: 0755]
tool/skeleton/skeleton.py [changed mode: 0755->0644]
tool/skeleton/skeleton_CLI.py [deleted file]

diff --git a/tool/skeleton/A_minus_B b/tool/skeleton/A_minus_B
new file mode 100755 (executable)
index 0000000..f6f7bbb
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+A_minus_B - CLI test driver for skeleton.tree_dict_A_minus_B(A, B)
+
+Usage:
+  A_minus_B <A_root> <B_root>
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Sequence
+
+import meta
+import skeleton
+
+
+def CLI(argv: Sequence[str] | None = None) -> int:
+  if argv is None:
+    argv = sys.argv[1:]
+
+  prog = os.path.basename(sys.argv[0]) if sys.argv else "A_minus_B"
+
+  if len(argv) != 2 or argv[0] in ("-h", "--help"):
+    print(f"Usage: {prog} <A_root> <B_root>")
+    return 1
+
+  A_root = argv[0]
+  B_root = argv[1]
+
+  if not os.path.isdir(A_root):
+    print(f"{prog}: {A_root}: not a directory")
+    return 2
+
+  if not os.path.isdir(B_root):
+    print(f"{prog}: {B_root}: not a directory")
+    return 3
+
+  A = skeleton.tree_dict_make(A_root, None)
+  B = skeleton.tree_dict_make(B_root, None)
+
+  meta.debug_set("tree_dict_A_minus_B")
+
+  _result = skeleton.tree_dict_A_minus_B(A, B)
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
diff --git a/tool/skeleton/Harmony_where b/tool/skeleton/Harmony_where
new file mode 100755 (executable)
index 0000000..9d39f1e
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+Harmony_where - CLI to locate the Harmony project root
+
+Usage:
+  Harmony_where
+
+Prints the status and path returned by Harmony.where().
+"""
+
+from __future__ import annotations
+
+import sys
+
+import Harmony
+
+
+def CLI(argv=None) -> int:
+  # Ignore argv; no arguments expected
+  status, Harmony_root = Harmony.where()
+
+  if status == "found":
+    print(f"Harmony project root found at: {Harmony_root}")
+    return 0
+
+  if status == "different":
+    print(f"Harmony not found, but nearest .git directory is: {Harmony_root}")
+    return 1
+
+  print("Harmony project root not found.")
+  return 2
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
diff --git a/tool/skeleton/in_between_and_below b/tool/skeleton/in_between_and_below
new file mode 100755 (executable)
index 0000000..2993767
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+in_between_and_below - CLI test driver for skeleton.tree_dict_in_between_and_below(A, B)
+
+Usage:
+  in_between_and_below <A_root> <B_root>
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Sequence
+
+import meta
+import skeleton
+
+
+def CLI(argv: Sequence[str] | None = None) -> int:
+  if argv is None:
+    argv = sys.argv[1:]
+
+  prog = os.path.basename(sys.argv[0]) if sys.argv else "in_between_and_below"
+
+  if len(argv) != 2 or argv[0] in ("-h", "--help"):
+    print(f"Usage: {prog} <A_root> <B_root>")
+    return 1
+
+  A_root = argv[0]
+  B_root = argv[1]
+
+  if not os.path.isdir(A_root):
+    print(f"{prog}: {A_root}: not a directory")
+    return 2
+
+  if not os.path.isdir(B_root):
+    print(f"{prog}: {B_root}: not a directory")
+    return 3
+
+  A = skeleton.tree_dict_make(A_root, None)
+  B = skeleton.tree_dict_make(B_root, None)
+
+  meta.debug_set("tree_dict_in_between_and_below")
+
+  _result = skeleton.tree_dict_in_between_and_below(A, B)
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
diff --git a/tool/skeleton/make_Harmony_tree_dict b/tool/skeleton/make_Harmony_tree_dict
new file mode 100755 (executable)
index 0000000..2ed3cea
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+skeleton_test - build and print the Harmony tree_dict
+
+Usage:
+  skeleton_test
+
+Behavior:
+  1. Locate the Harmony project root via Harmony.where().
+  2. Enable 'tree_dict_print' debug flag.
+  3. Call skeleton.tree_dict_make(Harmony_root, None).
+
+The skeleton.tree_dict_make() function is expected to call
+tree_dict_print() when the 'tree_dict_print' debug flag is set.
+"""
+
+from __future__ import annotations
+
+import sys
+
+import Harmony
+import meta
+import skeleton
+
+
+def CLI(argv=None) -> int:
+  # No arguments expected
+  status, Harmony_root = Harmony.where()
+
+  if status == "not-found":
+    print("Harmony project not found; cannot build tree_dict.")
+    return 1
+
+  if status == "different":
+    print("Warning: Harmony not found, using nearest .git directory for tree_dict.")
+
+  # Enable printing inside tree_dict_make
+  meta.debug_set("tree_dict_print")
+
+  _tree = skeleton.tree_dict_make(Harmony_root, None)
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
index 2025597..dee6439 100644 (file)
@@ -16,11 +16,13 @@ Current responsibilities:
 
 from __future__ import annotations
 
+import datetime
 from load_command_module import load_command_module
 
 
 # Load the incommon printenv module once at import time
 _PRINTENV_MODULE = load_command_module("printenv")
+_Z_MODULE = load_command_module("Z")
 
 
 # Meta module version
@@ -78,3 +80,18 @@ def printenv() -> int:
     ,[]        # names
     ,"printenv"
   )
+
+
+def z_format_mtime(
+  mtime: float
+) -> str:
+  """
+  Format a POSIX mtime (seconds since epoch, UTC) using the Z module.
+
+  Uses Z.ISO8601_FORMAT and Z.make_timestamp(dt=...).
+  """
+  dt = datetime.datetime.fromtimestamp(mtime, datetime.timezone.utc)
+  return _Z_MODULE.make_timestamp(
+    fmt=_Z_MODULE.ISO8601_FORMAT
+    ,dt=dt
+  )
diff --git a/tool/skeleton/newer b/tool/skeleton/newer
new file mode 100755 (executable)
index 0000000..30aa373
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+newer - CLI test driver for skeleton.tree_dict_newer(A, B)
+
+Usage:
+  newer <A_root> <B_root>
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Sequence
+
+import meta
+import skeleton
+
+
+def CLI(argv: Sequence[str] | None = None) -> int:
+  if argv is None:
+    argv = sys.argv[1:]
+
+  prog = os.path.basename(sys.argv[0]) if sys.argv else "newer"
+
+  if len(argv) != 2 or argv[0] in ("-h", "--help"):
+    print(f"Usage: {prog} <A_root> <B_root>")
+    return 1
+
+  A_root = argv[0]
+  B_root = argv[1]
+
+  if not os.path.isdir(A_root):
+    print(f"{prog}: {A_root}: not a directory")
+    return 2
+
+  if not os.path.isdir(B_root):
+    print(f"{prog}: {B_root}: not a directory")
+    return 3
+
+  A = skeleton.tree_dict_make(A_root, None)
+  B = skeleton.tree_dict_make(B_root, None)
+
+  meta.debug_set("tree_dict_newer")
+
+  _result = skeleton.tree_dict_newer(A, B)
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
diff --git a/tool/skeleton/older b/tool/skeleton/older
new file mode 100755 (executable)
index 0000000..f8ff24d
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+older - CLI test driver for skeleton.tree_dict_older(A, B)
+
+Usage:
+  older <A_root> <B_root>
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Sequence
+
+import meta
+import skeleton
+
+
+def CLI(argv: Sequence[str] | None = None) -> int:
+  if argv is None:
+    argv = sys.argv[1:]
+
+  prog = os.path.basename(sys.argv[0]) if sys.argv else "older"
+
+  if len(argv) != 2 or argv[0] in ("-h", "--help"):
+    print(f"Usage: {prog} <A_root> <B_root>")
+    return 1
+
+  A_root = argv[0]
+  B_root = argv[1]
+
+  if not os.path.isdir(A_root):
+    print(f"{prog}: {A_root}: not a directory")
+    return 2
+
+  if not os.path.isdir(B_root):
+    print(f"{prog}: {B_root}: not a directory")
+    return 3
+
+  A = skeleton.tree_dict_make(A_root, None)
+  B = skeleton.tree_dict_make(B_root, None)
+
+  meta.debug_set("tree_dict_older")
+
+  _result = skeleton.tree_dict_older(A, B)
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI())
old mode 100755 (executable)
new mode 100644 (file)
index d777a88..4799b64
@@ -28,7 +28,7 @@ import Harmony
 #   Values are dicts with:
 #     1. 'mtime'     : last modification time (float seconds)
 #     2. 'node_type' : 'file', 'directory', or 'other'
-#     3. 'dir_info'  : 'not-a-directory', 'leaf', 'project-root'
+#     3. 'dir_info'  : 'not-a-directory', 'leaf', 'root'
 #     4. 'checksum'  : present only for file nodes when checksum_fn is
 #                      not None
 #
@@ -46,6 +46,13 @@ def tree_dict_make(
     * Any path (directory or file) for which GitIgnore.check(<rel_path>)
       returns 'Ignore' is completely omitted from the tree_dict.
     * The root directory ('') is always included.
+    * Directory dir_info:
+        - 'root' for the root
+        - 'branch'        for directories that have child directories
+                          (after GitIgnore filtering)
+        - 'leaf'          for directories with no child directories
+    * Non-directory dir_info:
+        - 'NA'
   """
   root = os.path.abspath(path)
   gi = GitIgnore(root)
@@ -58,19 +65,29 @@ def tree_dict_make(
       rel_dir = ""
 
     # Skip ignored directories (except the root).
-    # We do this BEFORE recording the directory, so ignored dirs
-    # like '.git' or '__pycache__' never appear in tree_dict.
     if rel_dir != "" and gi.check(rel_dir) == "Ignore":
-      dirnames[:] = []   # do not descend
+      dirnames[:] = []
       continue
 
+    # Filter child directories by GitIgnore so dir_info reflects
+    # only directories we will actually traverse.
+    kept_dirnames: list[str] = []
+    for dn in list(dirnames):
+      child_rel = dn if rel_dir == "" else os.path.join(rel_dir, dn)
+      if gi.check(child_rel) == "Ignore":
+        dirnames.remove(dn)
+      else:
+        kept_dirnames.append(dn)
+
     # Record the directory node itself
     dir_abs = dirpath
     dir_mtime = os.path.getmtime(dir_abs)
     dir_node_type = "directory"
 
     if rel_dir == "":
-      dir_info = "project-root"
+      dir_info = "root"
+    elif kept_dirnames:
+      dir_info = "branch"
     else:
       dir_info = "leaf"
 
@@ -88,7 +105,6 @@ def tree_dict_make(
       else:
         rel_path = os.path.join(rel_dir, name)
 
-      # Filter files via GitIgnore as well.
       if gi.check(rel_path) == "Ignore":
         continue
 
@@ -102,12 +118,13 @@ def tree_dict_make(
       mtime = os.path.getmtime(abs_path)
 
       if node_type == "directory":
+        # Defensive; os.walk normally handles directories separately.
         if rel_path == "":
-          dir_info_f = "project-root"
+          dir_info_f = "root"
         else:
-          dir_info_f = "leaf"
+          dir_info_f = "branch"
       else:
-        dir_info_f = "not-a-directory"
+        dir_info_f = "NA"
 
       info: Dict[str, Any] = {
         "mtime": mtime
@@ -129,24 +146,77 @@ def tree_dict_print(
   tree_dict: Dict[str, Dict[str, Any]]
 ) -> None:
   """
-  Pretty-print a tree_dict produced by tree_dict_make().
+  Pretty-print a tree_dict produced by tree_dict_make() in fixed-width columns:
+
+    [type]  [dir]  [mtime]  [checksum?]  [relative path]
+
+  Only the values are printed in each column (no 'field=' prefixes).
+  mtime is formatted via the Z module for human readability.
   """
-  print("Tree dictionary contents:")
+  entries: list[tuple[str, str, str, str, str]] = []
+  has_checksum = False
+
   for rel_path in sorted(tree_dict.keys()):
     info = tree_dict[rel_path]
     display_path = rel_path if rel_path != "" else "."
 
-    parts = [
-      f"path={display_path}"
-      ,f"type={info.get('node_type')}"
-      ,f"mtime={info.get('mtime')}"
-      ,f"dir={info.get('dir_info')}"
-    ]
+    type_val = str(info.get("node_type", ""))
+    dir_val = str(info.get("dir_info", ""))
+
+    raw_mtime = info.get("mtime")
+    if isinstance(raw_mtime, (int, float)):
+      mtime_val = meta.z_format_mtime(raw_mtime)
+    else:
+      mtime_val = str(raw_mtime)
 
     if "checksum" in info:
-      parts.append(f"checksum={info['checksum']}")
+      checksum_val = str(info["checksum"])
+      has_checksum = True
+    else:
+      checksum_val = ""
+
+    entries.append((
+      type_val
+      ,dir_val
+      ,mtime_val
+      ,checksum_val
+      ,display_path
+    ))
+
+  # Compute column widths (values only)
+  type_w = 0
+  dir_w = 0
+  mtime_w = 0
+  checksum_w = 0
+
+  for type_val, dir_val, mtime_val, checksum_val, _ in entries:
+    if len(type_val) > type_w:
+      type_w = len(type_val)
+    if len(dir_val) > dir_w:
+      dir_w = len(dir_w) if False else len(dir_val)  # keep RT style simple
+    if len(mtime_val) > mtime_w:
+      mtime_w = len(mtime_val)
+    if has_checksum and len(checksum_val) > checksum_w:
+      checksum_w = len(checksum_val)
+
+  print("Tree dictionary contents:")
+  for type_val, dir_val, mtime_val, checksum_val, display_path in entries:
+    line = "  "
+
+    line += type_val.ljust(type_w)
+    line += "  "
+    line += dir_val.ljust(dir_w)
+    line += "  "
+    line += mtime_val.ljust(mtime_w)
+
+    if has_checksum:
+      line += "  "
+      line += checksum_val.ljust(checksum_w)
+
+    line += "  "
+    line += display_path
 
-    print("  " + ", ".join(parts))
+    print(line)
 
 
 def tree_dict_A_minus_B(
@@ -377,42 +447,3 @@ def tree_dict_older(
   return result
 
 
-def test_tree_dict() -> int:
-  """
-  Test helper for tree_dict_make:
-
-    1. Locate the Harmony project root.
-    2. Set the debug flag 'tree_dict_print'.
-    3. Call tree_dict_make() on the Harmony root with no checksum
-       function (checksum_fn=None).
-
-  The debug flag causes tree_dict_print() to be called automatically
-  inside tree_dict_make().
-  """
-  status, Harmony_root = Harmony.where()
-
-  if status == "not-found":
-    print("Harmony project not found; cannot test tree_dict_make.")
-    return 1
-
-  if status == "different":
-    print("Warning: Harmony not found, using nearest .git directory for tree_dict_make test.")
-
-  meta.debug_set("tree_dict_print")
-
-  _tree = tree_dict_make(Harmony_root, None)
-
-  return 0
-
-
-def test() -> int:
-  flag = 1
-  if not test_tree_dict():
-    print("fail: test_tree_dict")
-    flag = 0
-  return flag
-
-if __name__ == "__main__":
-  raise SystemExit(test())
-
-
diff --git a/tool/skeleton/skeleton_CLI.py b/tool/skeleton/skeleton_CLI.py
deleted file mode 100644 (file)
index b66b369..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-
-"""
-skeleton_CLI.py - generic CLI for skeleton A,B tree_dict functions
-
-Usage:
-  skeleton_CLI.py <function_name> <A_root> <B_root>
-
-Where <function_name> is one of:
-
-  - tree_dict_A_minus_B
-  - tree_dict_in_between_and_below
-  - tree_dict_newer
-  - tree_dict_older
-
-Behavior:
-  1. Build tree_dict A from <A_root>.
-  2. Build tree_dict B from <B_root>.
-  3. Enable the debug flag named <function_name>.
-  4. Call skeleton.<function_name>(A, B).
-
-Any printing is expected to be done by the called function when the
-corresponding debug flag is set.
-"""
-
-from __future__ import annotations
-
-import os
-import sys
-from typing import Sequence
-
-import meta
-import skeleton
-
-
-FUNCTIONS = {
-  "tree_dict_A_minus_B": skeleton.tree_dict_A_minus_B
-  ,"tree_dict_in_between_and_below": skeleton.tree_dict_in_between_and_below
-  ,"tree_dict_newer": skeleton.tree_dict_newer
-  ,"tree_dict_older": skeleton.tree_dict_older
-}
-
-
-def _print_usage(prog: str) -> None:
-  print(f"Usage: {prog} <function_name> <A_root> <B_root>")
-  print("Where <function_name> is one of:")
-  for name in sorted(FUNCTIONS.keys()):
-    print(f"  - {name}")
-
-
-def CLI(argv: Sequence[str] | None = None) -> int:
-  if argv is None:
-    argv = sys.argv[1:]
-
-  prog = os.path.basename(sys.argv[0]) if sys.argv else "skeleton_CLI.py"
-
-  if len(argv) != 3 or argv[0] in ("-h", "--help"):
-    _print_usage(prog)
-    return 1
-
-  func_name = argv[0]
-  A_root = argv[1]
-  B_root = argv[2]
-
-  func = FUNCTIONS.get(func_name)
-  if func is None:
-    print(f"{prog}: unknown function_name: {func_name}")
-    _print_usage(prog)
-    return 2
-
-  if not os.path.isdir(A_root):
-    print(f"{prog}: {A_root}: not a directory")
-    return 3
-
-  if not os.path.isdir(B_root):
-    print(f"{prog}: {B_root}: not a directory")
-    return 4
-
-  # Build tree_dicts
-  A = skeleton.tree_dict_make(A_root, None)
-  B = skeleton.tree_dict_make(B_root, None)
-
-  # Enable debug flag with the same name as the function
-  meta.debug_set(func_name)
-
-  # Call the function; any printing is done via debug hooks
-  _result = func(A, B)
-
-  return 0
-
-
-if __name__ == "__main__":
-  raise SystemExit(CLI())