checkpoint before rewrite due to apparent patch misalignment
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 27 Nov 2025 07:19:58 +0000 (07:19 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 27 Nov 2025 07:19:58 +0000 (07:19 +0000)
shared/authored/dir-walk/BreadthMachine.py [new file with mode: 0755]
shared/authored/dir-walk/DepthMachine.py [new file with mode: 0755]
shared/authored/dir-walk/Queue.py
shared/authored/dir-walk/TreeMachine.py
shared/authored/dir-walk/conventions.org [new file with mode: 0644]

diff --git a/shared/authored/dir-walk/BreadthMachine.py b/shared/authored/dir-walk/BreadthMachine.py
new file mode 100755 (executable)
index 0000000..5f20fbc
--- /dev/null
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+
+import sys
+from typing import List, Tuple
+
+import meta
+from TreeMachine import TreeMachine
+
+
+class BreadthMachine:
+  """
+  BreadthMachine — breadth-first directory walker built on TreeMachine.
+
+  External usage:
+
+    bm = BreadthMachine(root_dp)
+    while True:
+      path, props, addr = bm.read()
+      # ... process node ...
+      if not bm.can_step(): break
+      bm.step()
+  """
+
+  def __init__(self, root_dp: str | None = None) -> None:
+    if root_dp is None: root_dp = "."
+    self.root_dp: str = root_dp
+    self._tree_machine: TreeMachine = TreeMachine(root_dp)
+    self._has_node: bool = False
+
+    # Bootstrap: move to the first node, if any.
+    if self._tree_machine.context_depth() > 0:
+      self._tree_machine.step()
+      self._has_node = True
+
+  # ------------------------------------------------------------------
+  # External interface
+  # ------------------------------------------------------------------
+  def read(self) -> Tuple[str, List[str], Tuple[int, int]]:
+    """
+    Read the current node.
+
+    Returns:
+      (path_relative_to_root, property_list, address)
+    """
+    if not self._has_node:
+      raise RuntimeError("BreadthMachine.read() with no current node.")
+    return self._tree_machine.read()
+
+  def can_step(self) -> bool:
+    """
+    True if there is another node after the current one.
+
+    Logic:
+
+      - If current node is a non-empty directory, we can always push("back")
+        and eventually visit its children.
+
+      - Otherwise, delegate to TreeMachine.can_step(), which ripples through
+        all Contexts to see whether any Context is not rightmost.
+    """
+    if not self._has_node:
+      return False
+
+    _path, prop_list, _addr = self._tree_machine.read()
+    is_non_empty_dir = ("directory" in prop_list and "empty" not in prop_list)
+
+    if is_non_empty_dir:
+      return True
+
+    return self._tree_machine.can_step()
+
+  def step(self) -> None:
+    """
+    Advance to the next node (breadth-first flavor).
+
+    Steps:
+
+      1. If current node is a non-empty directory, push it to the BACK of
+         the context queue. (We do NOT descend immediately.)
+
+      2. If current context is not rightmost, step within it.
+
+      3. If current context is rightmost, pop contexts until a context is
+         found that can step, then step there.
+    """
+    if not self.can_step():
+      raise RuntimeError("BreadthMachine.step() called when can_step() is False.")
+
+    _path, prop_list, _addr = self._tree_machine.read()
+    is_non_empty_dir = ("directory" in prop_list and "empty" not in prop_list)
+
+    # 1. Queue non-empty directory for later (back of the queue).
+    if is_non_empty_dir:
+      self._tree_machine.push_current_directory("back")
+
+    # 2. Try to step within current context.
+    if not self._tree_machine.rightmost():
+      self._tree_machine.step()
+      return
+
+    # 3. Current context is rightmost -> pop until we can step.
+    while True:
+      self._tree_machine.pop_context()
+      if self._tree_machine.context_depth() == 0:
+        # Defensive: should not happen if can_step() was True.
+        self._has_node = False
+        return
+      if not self._tree_machine.rightmost():
+        self._tree_machine.step()
+        return
+      # Still rightmost in new front context; keep popping.
+
+  # ------------------------------------------------------------------
+  # Introspection helper
+  # ------------------------------------------------------------------
+  def has_node(self) -> bool:
+    """True if there is a current node to read."""
+    return self._has_node and self._tree_machine.context_depth() > 0
+
+
+def build_breadth_machine(root_dp: str) -> BreadthMachine:
+  """Worker: construct and return a BreadthMachine for root_dp."""
+  return BreadthMachine(root_dp)
+
+
+def CLI(argv: List[str]) -> int:
+  """
+  Command-line interface for BreadthMachine.
+
+  Usage:
+
+    ./BreadthMachine.py [root_dp]
+
+  If root_dp is omitted, "." is used.
+  """
+  root_dp = argv[1] if len(argv) > 1 else "."
+  bm = build_breadth_machine(root_dp)
+
+  print("BreadthMachine")
+  meta.version()
+  print(f"# root = {root_dp!r}")
+
+  if not bm.has_node():
+    print("# (no nodes)")
+    return 0
+
+  while True:
+    path, _props, _addr = bm.read()
+    print(path)
+    if not bm.can_step(): break
+    bm.step()
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI(sys.argv))
diff --git a/shared/authored/dir-walk/DepthMachine.py b/shared/authored/dir-walk/DepthMachine.py
new file mode 100755 (executable)
index 0000000..5d30f9f
--- /dev/null
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+
+import sys
+from typing import List, Tuple
+
+import meta
+from TreeMachine import TreeMachine
+
+
+class DepthMachine:
+  """
+  DepthMachine — depth-first directory walker built on TreeMachine.
+
+  External usage:
+
+    dm = DepthMachine(root_dp)
+    while True:
+      path, props, addr = dm.read()
+      # ... process node ...
+      if not dm.can_step(): break
+      dm.step()
+  """
+
+  def __init__(self, root_dp: str | None = None) -> None:
+    if root_dp is None: root_dp = "."
+    self.root_dp: str = root_dp
+    self._tree_machine: TreeMachine = TreeMachine(root_dp)
+    self._has_node: bool = False
+
+    # Bootstrap: move to the first node, if any.
+    if self._tree_machine.context_depth() > 0:
+      self._tree_machine.step()
+      self._has_node = True
+
+  # ------------------------------------------------------------------
+  # External interface
+  # ------------------------------------------------------------------
+  def read(self) -> Tuple[str, List[str], Tuple[int, int]]:
+    """
+    Read the current node.
+
+    Returns:
+      (path_relative_to_root, property_list, address)
+    """
+    if not self._has_node:
+      raise RuntimeError("DepthMachine.read() with no current node.")
+    return self._tree_machine.read()
+
+  def can_step(self) -> bool:
+    """
+    True if there is another node after the current one.
+
+    Logic:
+
+      - If current node is a non-empty directory, we can always push("front")
+        and descend into it.
+
+      - Otherwise, delegate to TreeMachine.can_step(), which ripples over
+        all Contexts to see whether any Context is not rightmost.
+    """
+    if not self._has_node:
+      return False
+
+    _path, prop_list, _addr = self._tree_machine.read()
+    is_non_empty_dir = ("directory" in prop_list and "empty" not in prop_list)
+
+    if is_non_empty_dir:
+      return True
+
+    return self._tree_machine.can_step()
+
+  def step(self) -> None:
+    """
+    Advance to the next node.
+
+    Cases:
+
+      A) Current node is a non-empty directory:
+           - push it to the FRONT of the context queue
+           - step into its first child.
+
+      B) Current node is not a non-empty directory AND current context
+         is not rightmost:
+           - step within current context.
+
+      C) Current node is not a non-empty directory AND current context
+         is rightmost:
+           - pop contexts until a non-rightmost context is found,
+             then step within that context.
+    """
+    if not self.can_step():
+      raise RuntimeError("DepthMachine.step() called when can_step() is False.")
+
+    _path, prop_list, _addr = self._tree_machine.read()
+    is_non_empty_dir = ("directory" in prop_list and "empty" not in prop_list)
+
+    # Case A: non-empty directory -> push front + descend.
+    if is_non_empty_dir:
+      self._tree_machine.push_current_directory("front")
+      # New front Context is the directory we just pushed; step into its first child.
+      self._tree_machine.step()
+      return
+
+    # Case B: not a non-empty dir and not rightmost -> simple step.
+    if not self._tree_machine.rightmost():
+      self._tree_machine.step()
+      return
+
+    # Case C: not a non-empty dir and rightmost -> pop until we can step.
+    while True:
+      self._tree_machine.pop_context()
+      if self._tree_machine.context_depth() == 0:
+        # Defensive: should not happen if can_step() was True.
+        self._has_node = False
+        return
+      if not self._tree_machine.rightmost():
+        self._tree_machine.step()
+        return
+      # Still rightmost in new front context; keep popping.
+
+  # ------------------------------------------------------------------
+  # Introspection helper
+  # ------------------------------------------------------------------
+  def has_node(self) -> bool:
+    """True if there is a current node to read."""
+    return self._has_node and self._tree_machine.context_depth() > 0
+
+
+def build_depth_machine(root_dp: str) -> DepthMachine:
+  """Worker: construct and return a DepthMachine for root_dp."""
+  return DepthMachine(root_dp)
+
+
+def CLI(argv: List[str]) -> int:
+  """
+  Command-line interface for DepthMachine.
+
+  Usage:
+
+    ./DepthMachine.py [root_dp]
+
+  If root_dp is omitted, "." is used.
+  """
+  root_dp = argv[1] if len(argv) > 1 else "."
+  dm = build_depth_machine(root_dp)
+
+  print("DepthMachine")
+  meta.version()
+  print(f"# root = {root_dp!r}")
+
+  if not dm.has_node():
+    print("# (no nodes)")
+    return 0
+
+  # Knuth-style loop: read, test, step.
+  while True:
+    path, _props, _addr = dm.read()
+    print(path)
+    if not dm.can_step(): break
+    dm.step()
+
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI(sys.argv))
index 2597423..da84999 100755 (executable)
@@ -99,6 +99,15 @@ class Queue(Generic[T]):
     else:
       self._queue_list = list(initial_iter)
 
+  def snapshot(self) -> list:
+    """
+    Return a shallow copy of the items in queue order (front to back).
+
+    This is intended for read-only traversal by higher-level logic
+    such as TreeMachine.can_step().
+    """
+    return list(self._items)
+
   # ----------------------------------------------------------------------
   # Basic properties
   # ----------------------------------------------------------------------
index 6e5eaf2..9057c3b 100755 (executable)
@@ -66,6 +66,35 @@ class Context:
     new_ctx._started = self._started
     return new_ctx
 
+  def can_step(self) -> bool:
+    """
+    True if a step/pop sequence would find another node without
+    exhausting the entire context queue.
+
+    Implementation:
+
+      - If the queue is empty, there is nothing left to visit.
+      - Otherwise, walk the Contexts from front to back:
+
+          * If we find any Context that is NOT rightmost, then there
+            exists some directory in the tree where a step can advance
+            (possibly after one or more pops).
+
+          * If ALL Contexts report rightmost(), then any attempt to
+            step/pop would eventually pop the entire queue, so there
+            is no next node.
+    """
+    if self._context_queue.is_empty():
+      return False
+
+    ctx_list = self._context_queue.snapshot()
+    for ctx in ctx_list:
+      if not ctx.rightmost():
+        return True
+
+    # Reached the back; every Context is rightmost.
+    return False
+
   def rightmost(self) -> bool:
     """
     True when this context cannot be stepped further without error.
diff --git a/shared/authored/dir-walk/conventions.org b/shared/authored/dir-walk/conventions.org
new file mode 100644 (file)
index 0000000..9f18ecc
--- /dev/null
@@ -0,0 +1,323 @@
+1. **2025-11-26 14:58:00Z**
+
+2. Here are cleaned-up versions of `DepthMachine.py` and `BreadthMachine.py`:
+
+* Single `if __name__ == "__main__":` branch.
+* A `CLI` function that:
+
+  * unpacks `argv`,
+  * calls a worker,
+  * returns an `int`.
+* A small worker function that can be called directly from other code without going through CLI packing/parsing.
+
+---
+
+### `DepthMachine.py`
+
+```python
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+
+import sys
+from typing import Any, List, Optional
+
+import meta
+from TapeMachine import TapeMachine
+from TreeMachine import TreeMachine
+
+
+def _depth_debug(dm: Any, msg: str) -> None:
+  """Emit a DepthMachine-related debug message if meta.DEBUG has 'DepthMachine'."""
+  if "DepthMachine" not in meta.DEBUG: return
+  print(f"[DepthMachine {hex(id(dm))}] {msg}")
+
+
+class DepthMachine(TapeMachine):
+  """
+  DepthMachine — depth-first directory walker as a TapeMachine.
+
+    - Wraps a TreeMachine.
+    - For each visited node, appends its relative path to a tape.
+    - For directories with children, pushes a Context with where_tag 'front'.
+  """
+
+  def __init__(self, root_dp: Optional[str] = None) -> None:
+    if root_dp is None: root_dp = "."
+    node_list: List[str] = self._build_node_list(root_dp)
+    if not node_list:
+      raise ValueError("DepthMachine requires at least one node under root.")
+    _depth_debug(self, f"init root_dp={root_dp!r}, n_nodes={len(node_list)}")
+    super().__init__(node_list)
+
+  def _build_node_list(self, root_dp: str) -> List[str]:
+    """Build a depth-first ordered list of node paths under root_dp."""
+    tm = TreeMachine(root_dp)
+    nodes: List[str] = []
+
+    while tm.context_depth() > 0:
+      tm.step()
+      value, prop_list, _addr = tm.read()
+      nodes.append(value)
+
+      if "directory" in prop_list and "empty" not in prop_list:
+        tm.push_current_directory("front")
+
+      if tm.rightmost():
+        tm.pop_context()
+
+    return nodes
+
+
+def build_depth_machine(root_dp: str) -> DepthMachine:
+  """Worker: construct and return a DepthMachine for root_dp."""
+  return DepthMachine(root_dp)
+
+
+def CLI(argv: List[str]) -> int:
+  """Command-line interface wrapper for DepthMachine."""
+  root_dp = argv[1] if len(argv) > 1 else "."
+  dm = build_depth_machine(root_dp)
+
+  print("DepthMachine")
+  meta.version()
+  print(f"# root = {root_dp!r}")
+  print(f"# length = {dm.length}")
+  dm.rewind()
+  while True:
+    print(dm.read())
+    if not dm.can_step(1): break
+    dm.step(1)
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI(sys.argv))
+```
+
+---
+
+### `BreadthMachine.py`
+
+```python
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+
+import sys
+from typing import Any, List, Optional
+
+import meta
+from TapeMachine import TapeMachine
+from TreeMachine import TreeMachine
+
+
+def _breadth_debug(bm: Any, msg: str) -> None:
+  """Emit a BreadthMachine-related debug message if meta.DEBUG has 'BreadthMachine'."""
+  if "BreadthMachine" not in meta.DEBUG: return
+  print(f"[BreadthMachine {hex(id(bm))}] {msg}")
+
+
+class BreadthMachine(TapeMachine):
+  """
+  BreadthMachine — breadth-first directory walker as a TapeMachine.
+
+    - Wraps a TreeMachine.
+    - For each visited node, appends its relative path to a tape.
+    - For directories with children, pushes a Context with where_tag 'back'.
+  """
+
+  def __init__(self, root_dp: Optional[str] = None) -> None:
+    if root_dp is None: root_dp = "."
+    node_list: List[str] = self._build_node_list(root_dp)
+    if not node_list:
+      raise ValueError("BreadthMachine requires at least one node under root.")
+    _breadth_debug(self, f"init root_dp={root_dp!r}, n_nodes={len(node_list)}")
+    super().__init__(node_list)
+
+  def _build_node_list(self, root_dp: str) -> List[str]:
+    """Build a breadth-first ordered list of node paths under root_dp."""
+    tm = TreeMachine(root_dp)
+    nodes: List[str] = []
+
+    while tm.context_depth() > 0:
+      tm.step()
+      value, prop_list, _addr = tm.read()
+      nodes.append(value)
+
+      if "directory" in prop_list and "empty" not in prop_list:
+        tm.push_current_directory("back")
+
+      if tm.rightmost():
+        tm.pop_context()
+
+    return nodes
+
+
+def build_breadth_machine(root_dp: str) -> BreadthMachine:
+  """Worker: construct and return a BreadthMachine for root_dp."""
+  return BreadthMachine(root_dp)
+
+
+def CLI(argv: List[str]) -> int:
+  """Command-line interface wrapper for BreadthMachine."""
+  root_dp = argv[1] if len(argv) > 1 else "."
+  bm = build_breadth_machine(root_dp)
+
+  print("BreadthMachine")
+  meta.version()
+  print(f"# root = {root_dp!r}")
+  print(f"# length = {bm.length}")
+  bm.rewind()
+  while True:
+    print(bm.read())
+    if not bm.can_step(1): break
+    bm.step(1)
+  return 0
+
+
+if __name__ == "__main__":
+  raise SystemExit(CLI(sys.argv))
+```
+
+You can now:
+
+```bash
+./DepthMachine.py
+./DepthMachine.py /some/path
+./BreadthMachine.py
+./BreadthMachine.py /some/path
+```
+
+And from another Python module:
+
+```python
+from DepthMachine import build_depth_machine
+
+dm = build_depth_machine("/some/path")
+dm.rewind()
+while True:
+  print(dm.read())
+  if not dm.can_step(1): break
+  dm.step(1)
+```
+
+---
+
+3. **RT conventions we’ve touched in this session**
+
+Here’s a quick index of the conventions that surfaced so far, so we don’t have to rediscover them later:
+
+1. **Indentation and layout**
+
+   * Two-space indent.
+   * `indent-tabs-mode: nil` in the file header.
+   * Multi-line functions kept visually tight; formal parameter lists on a single line where they comfortably fit.
+   * Short, “inline” style for simple methods when reasonable.
+
+2. **Naming style**
+
+   * PascalCase for type / class names: `TapeMachine`, `TreeMachine`, `DepthMachine`, `BreadthMachine`, `Context`, `Queue`.
+   * snake_case for functions, methods, and variables: `can_step`, `cue_end`, `build_depth_machine`, `root_dp`.
+   * DEBUG helpers: `_tape_debug`, `_tm_debug`, `_depth_debug`, `_breadth_debug`.
+
+3. **Path suffix conventions**
+
+   * `*_dp`   → directory path (unspecified relative vs absolute).
+   * `*_dpa`  → absolute directory path.
+   * `*_dpr`  → relative directory path.
+   * `*_fp`   → file path (unspecified).
+   * `*_fpa` / `*_fpr` → absolute / relative file paths.
+   * `*_fs_nod_pr` / `*_fs_nod_pa` → file-system node paths (relative / absolute) when type (file/dir/other) isn’t fixed.
+
+4. **TapeMachine semantics**
+
+   * First-rest pattern:
+
+     * Tape must be non-empty.
+     * Head (`_head_position`) always on a valid cell; starts at `0`.
+   * `can_step(n)` checks bounds; `step(n)` assumes caller checked.
+   * `read()` returns *just* the current value.
+   * `step()` returns nothing; it just moves the head.
+   * `cue()` is the public accessor for `_head_position`.
+   * `rewind()` sets cue to `0`.
+   * `cue_end()` sets cue to `length-1`.
+   * `entangle()` creates another machine sharing the same tape but with its own head.
+
+5. **Context / Queue conventions**
+
+   * Context objects hold:
+
+     * `dir_dpr` (relative directory path, `'.'` for root),
+     * `level` (depth, root == 1),
+     * child traversal state.
+   * `Context` internally uses a `TapeMachine` for child names; no separate index bookkeeping.
+   * Contexts are only created for directories with **non-empty** child lists.
+   * `Queue` holds `Context` objects and supports tagged pushes:
+
+     * `"front"` (depth-ish),
+     * `"back"` (breadth-ish),
+     * `"throw-away"` (discard).
+   * `Context.clone()` uses entangled `TapeMachine` for children.
+
+6. **TreeMachine semantics**
+
+   * Root stored as absolute `root_dpa`; external API uses paths relative to root.
+   * `step()` advances within the current `Context` and sets the current node.
+   * `read()` returns `(value, property_list, address)` where:
+
+     * `value` is a `fs_nod_pr` relative path,
+     * `property_list` ⊆ `{"directory","file","other","empty"}`,
+     * `address` is `(level, index)`; `level` not derived from path text.
+   * `rightmost()` checks if current context has any more nodes.
+   * `push_current_directory(where_tag)` enqueues non-empty directory contexts.
+   * `pop_context()` drops current context and clears the current head.
+   * `context_depth()` returns the active context depth.
+   * `entangle()` clones the context queue and current node/address, using `Context.clone()`.
+
+7. **CLI vs worker separation**
+
+   * Worker functions take structured arguments (e.g. `root_dp: str`) and do the real work:
+
+     * `build_depth_machine(root_dp)`,
+     * `build_breadth_machine(root_dp)`.
+   * `CLI(argv)`:
+
+     * unpacks `argv`,
+     * calls the worker,
+     * handles printing / side effects,
+     * returns an `int` status.
+   * `if __name__ == "__main__": raise SystemExit(CLI(sys.argv))` – single branch.
+
+8. **Debugging conventions**
+
+   * `meta.DEBUG` is a **set** of tags; use `add`, `discard`, membership tests.
+   * Debug helpers:
+
+     * Check presence of tag (`"TapeMachine"`, `"TreeMachine"`, `"Queue"`, `"DepthMachine"`, `"BreadthMachine"`, etc.).
+     * Print with class name and `hex(id(instance))` prefix:
+
+       * `[TapeMachine 0x...] msg`
+       * `[TreeMachine 0x...] msg`
+   * Tests can enable tags with:
+
+     ```python
+     meta.DEBUG.add("TapeMachine")
+     meta.DEBUG.add("TreeMachine")
+     ```
+
+9. **Versioning via `meta`**
+
+   * `meta` holds at least:
+
+     * `DEBUG` (set of tags),
+     * `version()` that prints `"version <major> <minor>"`.
+   * Modules print version in tests / CLI to track which build is running.
+
+If you’d like, next step could be a tiny “course” doc that shows:
+
+* raw `TreeMachine` usage,
+* `DepthMachine` / `BreadthMachine` as derived tapes,
+* and a short example of entangling and doing a sub-search without losing place.