.
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 2 Dec 2025 05:49:49 +0000 (05:49 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 2 Dec 2025 05:49:49 +0000 (05:49 +0000)
shared/authored/dir-walk.zip [new file with mode: 0644]
shared/authored/dir-walk/BreadthMachine.py
shared/authored/dir-walk/DepthMachine.py
shared/authored/dir-walk/JSON.py [new file with mode: 0755]
shared/authored/dir-walk/Queue.py
shared/authored/dir-walk/TapeMachine.py [changed mode: 0755->0644]
shared/authored/dir-walk/duck.py [new file with mode: 0755]
shared/authored/dir-walk/temp.py [new file with mode: 0644]
shared/authored/dir-walk/test_TapeMachine.py [new file with mode: 0755]

diff --git a/shared/authored/dir-walk.zip b/shared/authored/dir-walk.zip
new file mode 100644 (file)
index 0000000..e46a154
Binary files /dev/null and b/shared/authored/dir-walk.zip differ
index 5f20fbc..6d489e1 100755 (executable)
@@ -113,16 +113,17 @@ class BreadthMachine:
         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."""
+    """
+    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."""
+  """
+  Worker: construct and return a BreadthMachine for root_dp.
+  """
   return BreadthMachine(root_dp)
 
 
index 5d30f9f..ab6700a 100755 (executable)
@@ -121,16 +121,17 @@ class DepthMachine:
         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."""
+    """
+    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."""
+  """
+  Worker: construct and return a DepthMachine for root_dp.
+  """
   return DepthMachine(root_dp)
 
 
diff --git a/shared/authored/dir-walk/JSON.py b/shared/authored/dir-walk/JSON.py
new file mode 100755 (executable)
index 0000000..7a3336a
--- /dev/null
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+from typing import Any
+import json as pyjson
+import duck
+import builtins
+
+# ----------------------------------------------------------------------
+def to_JSON_TM(
+  tm: Any
+) -> str:
+  """
+  Convert a Tape Machine (TM_ND_A or compatible) to a JSON string.
+
+  JSON contains only:
+    - "tape": list of cell values
+    - "head": integer head position
+    - "extent": last valid index
+  """
+
+  head = tm.address()
+  tm.cue_leftmost()
+  tape_data = [tm.read(n) for n in range(0 ,tm.extent + 1)]
+
+  obj = {
+    "tape"  : tape_data
+    ,"head" : tm.address()
+    ,"extent": tm.extent
+  }
+
+  return pyjson.dumps(tm ,ensure_ascii=False)
+
+
+# ----------------------------------------------------------------------
+def print_TM(
+  tm: Any
+  ,indent: int = 0
+) -> None:
+  """
+  Print the JSON representation of the TM nicely indented.
+  """
+  s = to_JSON_TM(tm)
+  parsed = pyjson.loads(s)
+  out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False)
+  prefix = " " * indent
+  for line in out.split("\n"):
+    print(prefix + line)
+
+
+# ----------------------------------------------------------------------
+def pretty_TM(
+  tm0: Any
+  ,indent: int = 0
+) -> None:
+  """
+  Walk the tape cells, printing:
+
+      <indent><mark><value>
+
+  mark::
+      ' '  normal cell
+      '*'  head is here
+      '␀'  empty (None, non-head)
+  """
+  prefix = " " * indent
+
+  tm1 = tm0.entangle()
+  tm1.cue_leftmost()
+
+  while True:
+    v = tm1.read()
+
+    if tm1.address() == tm0.address():
+      mark = "*"
+    elif v is None:
+      mark = "␀"
+    else:
+      mark = " "
+
+    s = "" if v is None else str(v)
+    print(f"{prefix}{mark}{s}")
+
+    if not tm1.can_step(): break
+    tm1.step()
+
+
+# ----------------------------------------------------------------------
+def is_a_TM(
+  value: Any
+) -> bool:
+  """
+  A Tape Machine is anything providing the attributes:
+
+      entangle
+      cue_leftmost
+      read
+      can_step
+      step
+      address
+
+  These are exactly what pretty_TM() uses.
+  """
+  return duck.has(
+    value
+    ,"entangle"
+    ,"cue_leftmost"
+    ,"read"
+    ,"can_step"
+    ,"step"
+    ,"address"
+  )
+
+# ----------------------------------------------------------------------
+def print(
+  value: Any
+  ,indent: int = 0
+) -> None:
+  """
+  Print logic:
+
+    - If TM: call print_TM
+    - Else: print raw value using Python print
+      (JSON primitives remain one-line)
+  """
+  if is_a_TM(value):
+    print_TM(value ,indent)
+  else:
+    prefix = " " * indent
+    print(prefix + str(value))
+
+
+# ----------------------------------------------------------------------
+def pretty(
+  value: Any
+  ,indent: int = 0
+) -> None:
+  """
+  Pretty-print logic:
+
+    - If TM: pretty_TM
+    - If JSON primitive: print raw
+    - If non-JSON type: wrap into { "value": <repr> } and pretty-print
+    - If JSON object/array: pretty-print with indentation
+  """
+  prefix = " " * indent
+
+  # TM case
+  if is_a_TM(value):
+    pretty_TM(value ,indent)
+    return
+
+  # JSON primitive: str, int, float, bool, None
+  if isinstance(value ,(str ,int ,float ,bool)) or value is None:
+    print(prefix + str(value))
+    return
+
+  # Try to JSON-dump; if it fails, wrap in {"value": repr}
+  try:
+    js = pyjson.dumps(value ,ensure_ascii=False)
+    parsed = pyjson.loads(js)
+  except Exception:
+    wrapped = { "value": repr(value) }
+    parsed = wrapped
+
+  # Pretty-print JSON
+  out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False)
+  for line in out.split("\n"):
+    print(prefix + line)
+
+
+
+# ----------------------------------------------------------------------
+def CLI() -> None:
+  """
+  Demo for json helpers. Prints examples for:
+
+    - to_JSON_TM
+    - print_TM
+    - pretty_TM
+    - is_a_TM
+    - print
+    - pretty
+  """
+  p = builtins.print
+
+  class DemoTM:
+    def __init__(self) -> None:
+      self._tape = [1 ,None ,"three"]
+      self._head_position = 1  # on the None cell
+
+    @property
+    def extent(self) -> int:  return len(self._tape) - 1
+    def address(self) -> int: return self._head_position
+    def cue_leftmost(self) -> None: self._head_position = 0
+    def can_step(self ,n: int = 1) -> bool:
+      return 0 <= self._head_position + n <= self.extent
+    def step(self ,n: int = 1) -> None:
+      self._head_position += n
+    def read(self ,n: int = 0) -> Any:
+      return self._tape[self._head_position + n]
+    def write(self ,value: Any ,n: int = 0) -> None:
+      self._tape[self._head_position + n] = value
+    def entangle(self) -> "DemoTM":
+      tm = DemoTM()
+      tm._tape = self._tape
+      tm._head_position = self._head_position
+      return tm
+
+  tm = DemoTM()
+
+  p("json.is_a_TM examples:")
+  p("  is_a_TM(tm)   ->" ,is_a_TM(tm))
+  p("  is_a_TM(123)  ->" ,is_a_TM(123))
+  p()
+
+  p("json.to_JSON_TM example:")
+  p("  to_JSON_TM(tm) ->")
+  p("   " + to_JSON_TM(tm))
+  p()
+
+  p("json.print_TM example:")
+  print_TM(tm ,indent=2)
+  p()
+
+  p("json.pretty_TM example:")
+  pretty_TM(tm ,indent=2)
+  p()
+
+  p("json.print() examples:")
+  print(tm ,indent=2)                    # TM case
+  print(42 ,indent=2)                    # primitive
+  print({"a": 1 ,"b": 2} ,indent=2)      # dict, one-line
+  p()
+
+  p("json.pretty() examples:")
+  pretty(tm ,indent=2)                   # TM
+  p("---")
+  pretty(42 ,indent=2)                   # primitive
+  p("---")
+  pretty({"a": 1 ,"b": [2 ,3]} ,indent=2)  # JSON object
+  p("---")
+  pretty(object() ,indent=2)             # non-JSON -> wrapped
+
+
+# ----------------------------------------------------------------------
+if __name__ == "__main__":
+  CLI()
+
index da84999..5715169 100755 (executable)
 
 from __future__ import annotations
 
-from typing import Any, Generic, Iterable, List, Optional, TypeVar
-
 import meta
-
-
-T = TypeVar("T")
-
-
-def _format_item_for_debug(
-  item: Any
-) -> str:
-  """
-  Convert a queued item to a human-readable string for debug output.
-
-  If the item has a callable .print() method, we call it and use
-  the returned value (converted to str). Otherwise we fall back
-  to repr(item).
-  """
-  if hasattr(item, "print"):
-    candidate = getattr(item, "print")
-    if callable(candidate):
-      try:
-        text = candidate()
-        if text is None:
-          return repr(item)
-        return str(text)
-      except Exception:
-        return repr(item)
-  return repr(item)
+from typing import Any, List
 
 
 def _queue_debug(
-  queue: Any
-  ,op: str
-  ,item: Any | None = None
+  q: Any
+  ,msg: str
 ) -> None:
   """
-  Internal helper: emit a queue-related debug message if
-  the meta.DEBUG set contains the tag "Queue".
-
-  Includes the queue instance pointer in hex so we can
-  distinguish multiple Queue instances.
+  Emit a Queue-related debug message if meta.DEBUG has 'Queue'.
   """
-  if "Queue" not in meta.DEBUG:
-    return
-
-  qid = hex(id(queue))
-
-  if item is None:
-    print(f"[Queue {qid}] {op}")
-  else:
-    desc = _format_item_for_debug(item)
-    print(f"[Queue {qid}] {op}: {desc}")
+  if "Queue" not in meta.DEBUG: return
+  print(f"[Queue {hex(id(q))}] {msg}")
 
 
-class Queue(Generic[T]):
+class Queue:
   """
-  Queue — ordered queue of items (e.g., Context objects).
+  Simple FIFO queue with tagged push operations.
 
-  Semantics:
+  Items are stored in a Python list, with index 0 as the "front".
+  """
 
-    - The "front" of the queue is the current item.
-    - The "back" of the queue will eventually be visited.
-    - Popping an empty queue is an error.
+  def __init__(self) -> None:
+    self._items: List[Any] = []
 
-  Interface:
+  def push(
+    self
+    ,where_tag: str
+    ,item: Any
+  ) -> None:
+    """
+    Push an item into the queue.
 
-    front()  -> T    : current item (head), does not remove it.
-    back()   -> T    : last item in the queue, does not remove it.
-    top()    -> T    : alias for front().
-    depth()  -> int  : number of entries in the queue.
-    pop()    -> T    : remove and return the front item.
+    where_tag:
+      - 'front'       -> insert at front
+      - 'back'        -> append at back
+      - 'throw-away'  -> discard item (for debugging)
+    """
+    if hasattr(item, "print"):
+      desc = item.print()
+    else:
+      desc = repr(item)
 
-    push(where_tag, item: T) -> None:
+    if where_tag == "throw-away":
+      _queue_debug(self, f"push throw-away (discard): {desc}")
+      return
 
-      where_tag:
-        "front"      -> insert item at front of queue
-        "back"       -> append item at back of queue
-        "throw-away" -> do not store, treated as intentionally ignored
+    if where_tag == "front":
+      self._items.insert(0, item)
+      _queue_debug(self, f"push front: {desc}")
+      return
 
-    clone() -> Queue[T]
+    if where_tag == "back":
+      self._items.append(item)
+      _queue_debug(self, f"push back: {desc}")
+      return
 
-      Creates a copy of the Queue. For each item:
-        - If item has a .clone() method, it is called.
-        - Otherwise the item reference is reused.
-  """
+    raise ValueError(f"Queue.push: unknown where_tag {where_tag!r}")
 
-  def __init__(
-    self
-    ,initial_iter: Optional[Iterable[T]] = None
-  ) -> None:
-    if initial_iter is None:
-      self._queue_list: List[T] = []
+  def pop(self) -> Any:
+    """
+    Pop and return the front item.
+    """
+    if not self._items:
+      raise IndexError("Queue.pop on empty queue.")
+    item = self._items.pop(0)
+    if hasattr(item, "print"):
+      desc = item.print()
     else:
-      self._queue_list = list(initial_iter)
+      desc = repr(item)
+    _queue_debug(self, f"pop: {desc}")
+    return item
 
-  def snapshot(self) -> list:
+  def front(self) -> Any:
     """
-    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 the front item without removing it.
     """
-    return list(self._items)
-
-  # ----------------------------------------------------------------------
-  # Basic properties
-  # ----------------------------------------------------------------------
-  def is_empty(self) -> bool:
-    return len(self._queue_list) == 0
-
-  def depth(self) -> int:
-    return len(self._queue_list)
+    if not self._items:
+      raise IndexError("Queue.front on empty queue.")
+    return self._items[0]
 
-  # ----------------------------------------------------------------------
-  # Cloning
-  # ----------------------------------------------------------------------
-  def clone(self) -> "Queue[T]":
+  def back(self) -> Any:
     """
-    Create a copy of this Queue.
-
-    The new queue has its own internal list. If an item provides
-    a .clone() method, that is used to copy the item; otherwise
-    the item reference is reused.
+    Return the back item without removing it.
     """
-    new_item_list: List[T] = []
-    for item in self._queue_list:
-      if hasattr(item, "clone") and callable(getattr(item, "clone")):
-        new_item_list.append(item.clone())  # type: ignore[arg-type]
-      else:
-        new_item_list.append(item)
-    new_q = Queue[T](new_item_list)
-    _queue_debug(self, f"clone -> new {hex(id(new_q))}")
-    return new_q
-
-  # ----------------------------------------------------------------------
-  # Accessors
-  # ----------------------------------------------------------------------
-  def front(self) -> T:
-    if self.is_empty():
-      raise RuntimeError("Queue.front() on empty queue.")
-    return self._queue_list[0]
-
-  def back(self) -> T:
-    if self.is_empty():
-      raise RuntimeError("Queue.back() on empty queue.")
-    return self._queue_list[-1]
+    if not self._items:
+      raise IndexError("Queue.back on empty queue.")
+    return self._items[-1]
 
-  def top(self) -> T:
+  def depth(self) -> int:
     """
-    Alias for front().
+    Number of items in the queue.
     """
-    return self.front()
+    return len(self._items)
 
-  # ----------------------------------------------------------------------
-  # Mutating operations
-  # ----------------------------------------------------------------------
-  def pop(self) -> T:
+  def is_empty(self) -> bool:
     """
-    Remove and return the front item.
-
-    Popping an empty queue is an error.
+    True if the queue is empty.
     """
-    if self.is_empty():
-      raise RuntimeError("Queue.pop() on empty queue.")
-    item = self._queue_list.pop(0)
-    _queue_debug(self, "pop", item)
-    return item
+    return not self._items
 
-  def push(
-    self
-    ,where_tag: str
-    ,item: T
-  ) -> None:
+  def snapshot(self) -> list:
     """
-    Push an item according to where_tag:
-
-      where_tag == "front":
-        insert item at front of the queue.
-
-      where_tag == "back":
-        append item at back of the queue.
+    Return a shallow copy of items (front to back).
 
-      where_tag == "throw-away":
-        do not store item; treated as intentionally ignored.
-
-    Any other where_tag is an error.
+    Intended for read-only inspection (e.g. TreeMachine.can_step()).
     """
-    if where_tag == "front":
-      self._queue_list.insert(0, item)
-      _queue_debug(self, "push front", item)
-      return
-
-    if where_tag == "back":
-      self._queue_list.append(item)
-      _queue_debug(self, "push back", item)
-      return
-
-    if where_tag == "throw-away":
-      _queue_debug(self, "push throw-away (discard)", item)
-      return
-
-    raise ValueError(f"Unknown Queue push tag: {where_tag!r}")
+    return list(self._items)
 
-  # ----------------------------------------------------------------------
-  # Dispatcher
-  # ----------------------------------------------------------------------
-  def dispatch(
-    self
-    ,op: str
-    ,*args
-    ,**kwargs
-  ):
+  def clone(self) -> "Queue":
     """
-    Generic dispatcher for method-tag control.
-
-    Supported op values:
-
-      "front" -> front()
-      "back"  -> back()
-      "top"   -> top()
-      "depth" -> depth()
-      "pop"   -> pop()
+    Clone the queue.
 
-      "push"  -> push(where_tag, item)
-
-    Any unknown op raises ValueError.
+    If an item has a .clone() method, use it. Otherwise copy the
+    reference directly.
     """
-    if op == "front":
-      return self.front()
-    if op == "back":
-      return self.back()
-    if op == "top":
-      return self.top()
-    if op == "depth":
-      return self.depth()
-    if op == "pop":
-      return self.pop()
-    if op == "push":
-      if len(args) != 2:
-        raise ValueError("dispatch('push', where_tag, item) requires 2 arguments.")
-      where_tag = args[0]
-      item = args[1]
-      self.push(where_tag, item)
-      return None
-
-    raise ValueError(f"Unknown Queue operation: {op!r}")
-
-
-# ----------------------------------------------------------------------
-# Simple test harness
-# ----------------------------------------------------------------------
-class TestItem:
-  """
-  Minimal test item for the __main__ test().
-
-  It implements .print() so Queue can use it in debug messages.
-  """
-
-  def __init__(
-    self
-    ,name: str
-  ) -> None:
-    self.name = name
-
-  def print(self) -> str:
-    return f"TestItem({self.name})"
-
-  def clone(self) -> "TestItem":
-    # For the test, clone is just another instance with same name.
-    return TestItem(self.name)
+    new_q = Queue()
+    for item in self._items:
+      if hasattr(item, "clone"):
+        new_q._items.append(item.clone())
+      else:
+        new_q._items.append(item)
+    return new_q
 
 
-def test() -> None:
+def CLI(argv: list[str]) -> int:
   """
-  Simple test for Queue with debug enabled.
+  Simple self-test for Queue.
   """
-  meta.DEBUG.add("Queue")
-
   print("Queue test starting...")
   meta.version()
+  meta.DEBUG.add("Queue")
 
-  q = Queue[TestItem]()
-  q2 = q.clone()  # exercise clone-debug once at the start
-
-  q.push("front", TestItem("root"))
-  q.push("back", TestItem("child-A"))
-  q.push("back", TestItem("child-B"))
-  q.push("throw-away", TestItem("ignored-node"))
+  q = Queue()
+  q.push("back", "apple")
+  q.push("back", "banana")
+  q.push("front", "cherry")
+  q.push("throw-away", "ignored")
 
   print(f"depth = {q.depth()}")
-  print(f"front = {q.front().print()}")
-  print(f"back  = {q.back().print()}")
+  print(f"front = {q.front()!r}")
+  print(f"back  = {q.back()!r}")
 
   popped = q.pop()
-  print(f"popped = {popped.print()}")
+  print(f"popped = {popped!r}")
   print(f"depth  = {q.depth()}")
 
   print("Queue test done.")
+  return 0
 
 
 if __name__ == "__main__":
-  test()
+  import sys
+  raise SystemExit(CLI(sys.argv))
old mode 100755 (executable)
new mode 100644 (file)
index 0ca1d21..abf2e9b
 # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
 
 from __future__ import annotations
+from typing import Any
+import duck
 
-from typing import Any, Optional
-
-import meta
-
+class TM_ND_A:
+  """
+  TM_ND_A
 
-def _tape_debug(tm: Any, msg: str) -> None:
-  """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'."""
-  if "TapeMachine" not in meta.DEBUG: return
-  print(f"[TapeMachine {hex(id(tm))}] {msg}")
+    TM  Tape Machine
+    ND  Non Destructive, no cell deletion or insertion
+    A   Address supports addressing of cells
 
+    Machine does not support status, so if the machine exists, the tape has
+    at least one cell.
 
-class TapeMachine:
+    Head starts on cell 0 and by contract with the user is always on a valid cell.
+    I.e. it is the user's responsibility to check `can_step` before stepping.
   """
-  TapeMachine — single-tape, single-head, first-rest pattern.
 
-    - Tape is non-empty.
-    - Head starts on cell 0 and is always on a valid cell.
-    - Movement contract: caller checks can_step(n) before step(n).
-  """
-
-  def __init__(self, tape: Optional[Any] = None) -> None:
-    if tape is None: tape = []
-    for name in ("__len__", "__getitem__", "__setitem__"):
-      if not hasattr(tape, name): raise TypeError(f"TapeMachine tape must support {name}: {tape!r}")
-    self._length: int = len(tape)
-    if self._length == 0: raise ValueError("TapeMachine requires a non-empty tape (first-rest pattern).")
+  # ----------------------------------------------------------------------
+  def __init__(self ,tape):
+    duck.require(duck.class_of(tape) ,"__len__" ,"__getitem__" ,"__setitem__")
+    self._extent: int = len(tape) - 1
     self._tape = tape
     self._head_position: int = 0
-    _tape_debug(self, f"init length={self._length}, head_position=0")
 
+  # ----------------------------------------------------------------------
   @property
-  def length(self) -> int:
-    """Length of the tape (number of cells)."""
-    return self._length
-
-  def cue(self, new_position: Optional[int] = None) -> int:
-    """Get or set head (cue) position."""
-    if new_position is None: return self._head_position
-    if not 0 <= new_position < self._length: raise IndexError(f"TapeMachine.cue: out of range ({new_position}).")
-    self._head_position = new_position
-    return self._head_position
-
-  def can_step(self, n: int = 1) -> bool:
-    """True if step(n) would keep the head on the tape."""
-    proposed_position = self._head_position + n
-    return 0 <= proposed_position < self.length
-
-  def rewind(self) -> None:
-    """Move head to the first cell (index 0)."""
-    self._head_position = 0
-
-  def cue_end(self) -> None:
-    """Move head to the last cell (index length-1)."""
-    self._head_position = self._length - 1
-
-  def step(self, n: int = 1) -> None:
-    """Move head by n cells (n>0 right, n<0 left, n=0 no-op); caller must ensure can_step(n)."""
-    self._head_position += n
-
-  def read(self) -> Any:
-    """Read value under head."""
-    return self._tape[self._head_position]
-
-  def write(self, value: Any) -> None:
-    """Write a new value into the current cell."""
-    self._tape[self._head_position] = value
-
-  def print(self) -> str:
-    """
-    Printable view of the tape.
-
-      Two spaces then value per line; line with the head is prefixed '* '.
-    """
-    line_list = []
-    for idx in range(self._length):
-      prefix = "* " if idx == self._head_position else "  "
-      line_list.append(f"{prefix}{self._tape[idx]}")
-    return "\n".join(line_list)
-
+  def        extent(self) -> int:  return self._extent
+  def          tape(self) -> Any:  return self._tape
+  def         where(self) -> int:  return self._head_position
+  def  cue_leftmost(self) -> None: self._head_position = 0
+  def cue_rightmost(self) -> None: self._head_position = self._extent
+  def           cue(self ,new_position: int) -> int: self._head_position = new_position
+  def      can_step(self ,n: int = 1) -> bool: return 0 <= self._head_position + n <= self._extent
+  def          step(self ,n: int = 1) -> None: self._head_position += n
+  def          read(self ,n: int = 0) -> Any:  return self._tape[self._head_position + n]
+  def         write(self ,value: Any ,n: int = 0) -> None: self._tape[self._head_position + n] = value
+
+  # because this is an ND machine it is safe to entangle heads
   def entangle(self) -> "TapeMachine":
     """Create an entangled copy: shared tape, independent head_position."""
     new_tm = object.__new__(TapeMachine)
     new_tm._tape = self._tape
     new_tm._length = self._length
     new_tm._head_position = self._head_position
-    _tape_debug(self, f"entangle -> new {hex(id(new_tm))} head_position={self._head_position}")
     return new_tm
 
-
-def test() -> None:
-  """Simple TapeMachine test with first-rest semantics."""
-  meta.DEBUG.add("TapeMachine")
-  print("TapeMachine test starting..."); meta.version()
-
-  tm = TapeMachine(["apple", "orange", "tangerine"])
-  print(f"initial: cue={tm.cue()}, value={tm.read()!r}")
-
-  while tm.can_step(1):
-    tm.step(1)
-    print(f"step(+1): cue={tm.cue()}, value={tm.read()!r}")
-
-  if tm.can_step(-1):
-    tm.step(-1)
-    print(f"step(-1): cue={tm.cue()}, value={tm.read()!r}")
-
-  tm.rewind()
-  print(f"rewind: cue={tm.cue()}, value={tm.read()!r}")
-
-  tm2 = tm.entangle()
-  if tm.can_step(2):
-    tm.step(2)
-    print(f"tm step(+2): cue={tm.cue()}, value={tm.read()!r}")
-  if tm2.can_step(1):
-    tm2.step(1)
-    print(f"tm2 step(+1): cue={tm2.cue()}, value={tm2.read()!r}")
-
-  tm.cue_end()
-  print(f"cue_end: cue={tm.cue()}, value={tm.read()!r}")
-
-  print("print(tm):")
-  print(tm.print())
-  print("TapeMachine test done.")
-
-
-if __name__ == "__main__":
-  test()
diff --git a/shared/authored/dir-walk/duck.py b/shared/authored/dir-walk/duck.py
new file mode 100755 (executable)
index 0000000..cc086d6
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from typing import Any, Optional
+
+
+# ----------------------------------------------------------------------
+def class_of(
+  value: Any
+) -> Optional[type]:
+  """
+  Return the runtime class for a value.
+
+  If the value has a __class__ attribute that is a real type, return it.
+  Otherwise return None.
+  """
+  cls = getattr(value ,"__class__" ,None)
+  return cls if isinstance(cls ,type) else None
+
+
+# ----------------------------------------------------------------------
+def has(
+  inspected_class: Any
+  ,*attribute_names: str
+) -> bool:
+  """
+  True iff `inspected_class` has *all* listed attributes.
+
+  Usage:
+      has(MyClass ,"x" ,"y" ,"z")
+  """
+  for name in attribute_names:
+    if not hasattr(inspected_class ,name):
+      return False
+  return True
+
+
+# ----------------------------------------------------------------------
+def require(
+  inspected_class: Any
+  ,*attribute_names: str
+) -> None:
+  """
+  Guard: ensure `inspected_class` has the required attributes.
+
+  Usage:
+      require(MyClass ,"x" ,"y" ,"z")
+
+  Raise TypeError if any required attribute is missing.
+  """
+  if has(inspected_class ,*attribute_names):
+    return
+
+  # Collect missing names for detailed error.
+  missing_names = [
+    name
+    for name in attribute_names
+    if not hasattr(inspected_class ,name)
+  ]
+
+  raise TypeError(
+    f"require() failed: {inspected_class!r} "
+    f"is missing attributes {missing_names!r}"
+  )
+
+# ----------------------------------------------------------------------
+def CLI() -> None:
+  """
+  Demo for duck helpers. Prints examples for:
+
+    - class_of
+    - has
+    - require
+  """
+
+  class Demo:
+    demo_attr = 1
+    def demo_method(self) -> None:  # short-stuff
+      pass
+
+  print("duck.class_of examples:")
+  print("  class_of(Demo)        ->" ,class_of(Demo))
+  print("  class_of(Demo())      ->" ,class_of(Demo()))
+  print("  class_of(None)        ->" ,class_of(None))
+  print()
+
+  print("duck.has examples:")
+  print("  has(Demo ,'demo_attr')          ->"
+        ,has(Demo ,"demo_attr"))
+  print("  has(Demo ,'demo_method')        ->"
+        ,has(Demo ,"demo_method"))
+  print("  has(Demo ,'missing_attr')       ->"
+        ,has(Demo ,"missing_attr"))
+  print("  has(Demo ,'demo_attr','missing_attr') ->"
+        ,has(Demo ,"demo_attr" ,"missing_attr"))
+  print()
+
+  print("duck.require examples:")
+  print("  require(Demo ,'demo_attr')      (no error)")
+  require(Demo ,"demo_attr")
+
+  print("  require(Demo ,'demo_attr','demo_method') (no error)")
+  require(Demo ,"demo_attr" ,"demo_method")
+
+  print("  require(Demo ,'missing_attr')   (should raise)")
+  try:
+    require(Demo ,"missing_attr")
+  except TypeError as exc:
+    print("    caught TypeError:" ,exc)
+
+
+# ----------------------------------------------------------------------
+if __name__ == "__main__":
+  CLI()
diff --git a/shared/authored/dir-walk/temp.py b/shared/authored/dir-walk/temp.py
new file mode 100644 (file)
index 0000000..c546377
--- /dev/null
@@ -0,0 +1,98 @@
+
+  to_JSON:
+    For each cell of the tape:
+    1. If the value has a to_JSON() method, use value.to_JSON().
+    2. If the value is a JSON primitive (str,int,float,bool,None), output it directly.
+    3. If the value is a Python primitive that is not a JSON primitive, stringify it
+       with JSON-safe escaping.
+    4. Otherwise: error — cannot represent in JSON.
+
+  # ----------------------------------------------------------------------
+  # JSON representation
+  # ----------------------------------------------------------------------
+
+  @classmethod
+  def is_a_JSON_primitive(
+    cls
+    ,value: Any
+  ) -> bool:
+    return (
+      isinstance(value ,(str ,int ,float ,bool))
+      or value is None
+    )
+
+  @classmethod
+  def has_to_JSON(
+    cls
+    ,value: Any
+  ) -> bool:
+    return hasattr(value ,"to_JSON") and callable(value.to_JSON)
+
+  @classmethod
+  def normalize_value(
+    cls
+    ,value: Any
+  ) -> Any:
+    """
+    Normalize a tape cell value into JSON-compatible output.
+
+    Rules:
+      1. If the value has to_JSON, call it.
+      2. If the value is a JSON primitive, return it.
+      3. If the value is a Python primitive but not JSON-safe, stringify it with escaping.
+      4. Else: raise TypeError.
+    """
+    # Rule 1: to_JSON
+    if cls.has_to_JSON(value):
+      return value.to_JSON()
+
+    # Rule 2: JSON primitive
+    if cls.is_a_JSON_primitive(value):
+      return value
+
+    # Rule 3: Python primitive → stringify
+    # Note: repr() is safe because JSONPretty will escape the resulting string.
+    if isinstance(value ,(bytes ,bytearray)):
+      # represent bytes safely in hex
+      hexval = value.hex()
+      return f"<bytes len={len(value)} hex={hexval}>"
+
+    if isinstance(value ,(complex ,range)):
+      return repr(value)
+
+    # Rule 4: No idea what this is
+    raise TypeError(f"TapeMachine cannot JSON-normalize value: {value!r} of type {type(value)}")
+
+  def to_JSON(
+    self
+  ) -> dict[str ,Any]:
+    """
+    JSON representation:
+
+      {
+        "TapeMachine": "<hex id>"
+        ,"head": <int>
+        ,"Tape": [ <Value0> ,<Value1> , ... ]
+      }
+
+    Where each Value is JSON-normalized by normalize_value().
+    """
+    normalized = []
+    for raw in self._tape:
+      normalized.append(self.normalize_value(raw))
+
+    return {
+      "TapeMachine": hex(id(self))
+      ,"head": self._head_position
+      ,"Tape": normalized
+    }
+
+def _tape_debug(
+  tm: Any
+  ,msg: str
+) -> None:
+  """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'."""
+  if "TapeMachine" not in meta.DEBUG: return
+  print(f"[TapeMachine {hex(id(tm))}] {msg}")
+
+
diff --git a/shared/authored/dir-walk/test_TapeMachine.py b/shared/authored/dir-walk/test_TapeMachine.py
new file mode 100755 (executable)
index 0000000..8701c64
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; -*-
+
+from TapeMachine import TapeMachine
+from JSONPretty import print_JSON ,pretty_JSON
+import meta
+
+
+def test_TapeMachine() -> None:
+  """Unit test for TapeMachine using JSONPretty output."""
+  meta.DEBUG.add("TapeMachine")
+  print("TapeMachine test starting..."); meta.version()
+
+  print("----------------------------------------")
+  print("instantiating tape machine")
+  print("")
+
+  tm = TapeMachine(["apple", "orange", "tangerine"])
+
+  print("----------------------------------------")
+  print("printing newly instantiated tape machine")
+  print("")
+
+  print_JSON(tm.to_JSON())
+
+
+  print("----------------------------------------")
+  print("pretty printing newly instantiated tape machine")
+  print("")
+
+  pretty_JSON(tm.to_JSON())
+
+  print("----------------------------------------")
+  print("one rightward step at a time through the tape")
+  print("")
+
+  while tm.can_step(1):
+    tm.step(1)
+    print_JSON(tm.to_JSON())
+
+  print("----------------------------------------")
+  print("one leftward step at a time through the tape")
+  print("")
+
+  if tm.can_step(-1):
+    tm.step(-1)
+    print_JSON(tm.to_JSON())
+
+  print("----------------------------------------")
+  print("printing machine again")
+  print("")
+
+  tm.rewind()  # this won't do anything
+  print_JSON(tm.to_JSON())
+
+  print("----------------------------------------")
+  print("making entangled copy")
+  print("")
+
+  tm2 = tm.entangle()
+
+  print("----------------------------------------")
+  print("stepping original two steps")
+  print("")
+  if tm.can_step(2):
+    tm.step(2)
+    print_JSON(tm.to_JSON())
+
+  print("----------------------------------------")
+  print("stepping copy one step")
+  print("")
+  if tm2.can_step(1):
+    tm2.step(1)
+    print_JSON(tm2.to_JSON())
+
+  tm.cue_end()
+  print_JSON(tm.to_JSON())
+
+  print("TapeMachine test done.")
+
+
+if __name__ == "__main__":
+  test_TapeMachine()