passes small test
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 20 Sep 2025 16:34:53 +0000 (16:34 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 20 Sep 2025 16:34:53 +0000 (16:34 +0000)
30 files changed:
developer/source/Man_In_Grey
developer/source/Man_In_Grey.py
developer/source/executor_inner.py
document/todo.txt [new file with mode: 0644]
release/python3/Man_In_Grey.py
release/python3/executor_inner.py
release/shell/Man_In_Grey
tester/test_0/Man_In_Grey_input_acceptance.py [new file with mode: 0644]
tester/test_0/cat_stage [new file with mode: 0755]
tester/test_0/clean_out [new file with mode: 0755]
tester/test_0/ls_stage [new file with mode: 0755]
tester/test_0/run
tester/test_0/run_check [new file with mode: 0755]
tester/test_0/run_check.transcript [new file with mode: 0644]
tester/test_0/stage_test_0_out/.githolder [deleted file]
tester/test_0/stage_test_0_out/net/unbound.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out/unbound_conf [new file with mode: 0644]
tester/test_0/stage_test_0_out/unbound_conf.20250920T161232Z [new file with mode: 0644]
tester/test_0/stage_test_0_out/web/nginx.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out/web/nginx.conf.20250920T161233Z [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_0/net/unbound.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_0/unbound_conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_0/web/nginx.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_1/net/unbound.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_1/unbound_conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_1/unbound_conf.20250920T160648Z [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_1/web/nginx.conf [new file with mode: 0644]
tester/test_0/stage_test_0_out_expected_1/web/nginx.conf.20250920T160648Z [new file with mode: 0644]
tester/tool/env
tool_shared/document/install_Python.org

index d4180e3..2ad1606 100644 (file)
@@ -1,57 +1,62 @@
 #!/usr/bin/env bash
-# Man_In_Grey — canonical entrypoint for Man_In_Grey
-# - Resolves repo root via this script’s location (…/release/shell/)
-# - Picks gasket at release/<arch>/man_in_grey_apply when present
-# - Falls back to Python inner executor
-# - Always invokes the Python orchestrator Man_In_Grey.py
-
+# man_in_grey — canonical entrypoint for Man_In_Grey
+# - Resolves repo → release dirs
+# - Requires the privileged gasket (single code path)
+# - Always invokes the Python orchestrator with --apply-cmd <gasket>
 set -euo pipefail
 
-# --- resolve paths ---
-_this="${BASH_SOURCE[0]}"
+# --- resolve this script’s absolute path (portable) ---
+_this="${BASH_SOURCE[0]:-$0}"
+
 if command -v realpath >/dev/null 2>&1; then
-  _this_abs="$(realpath "$_this")"
+  _this_abs="$(realpath -- "$_this")"
 else
-  _this_abs="$(readlink -f "$_this" 2>/dev/null || (cd "$(dirname "$_this")" && pwd -P)/"$(basename "$_this"))"
+  if _tmp="$(readlink -f -- "$_this" 2>/dev/null)"; then
+    _this_abs="${_tmp}"
+  else
+    _dir="$(cd "$(dirname -- "$_this")" && pwd -P)"
+    _base="$(basename -- "$_this")"
+    _this_abs="${_dir}/${_base}"
+  fi
 fi
 
-_shell_dir="$(cd "$(dirname "$_this_abs")" && pwd -P)"   # .../release/shell
-_release_dir="$(cd "$_shell_dir/.." && pwd -P)"         # .../release
-_repo_root="$(cd "$_release_dir/.." && pwd -P)"         # repo root
-
-_py_release="$_release_dir/python3"
-_py_dev="$_repo_root/developer/source"
-
-_py_entry=""
-if [[ -f "$_py_release/Man_In_Grey.py" ]]; then
-  _py_entry="$_py_release/Man_In_Grey.py"
-elif [[ -f "$_py_dev/Man_In_Grey.py" ]]; then
-  _py_entry="$_py_dev/Man_In_Grey.py"
+_shell_dir="$(cd "$(dirname -- "$_this_abs")" && pwd -P)"   # .../release/shell
+_release_dir="$(cd "${_shell_dir}/.." && pwd -P)"           # .../release
+_repo_root="$(cd "${_release_dir}/.." && pwd -P)"           # repo root
+
+# --- orchestrator path (prefer released copy) ---
+_py_release="${_release_dir}/python3/Man_In_Grey.py"
+_py_dev="${_repo_root}/developer/source/Man_In_Grey.py"
+if [[ -f "$_py_release" ]]; then
+  _py_entry="$_py_release"
+elif [[ -f "$_py_dev" ]]; then
+  _py_entry="$_py_dev"
 else
   echo "error: Man_In_Grey.py not found in release/python3/ or developer/source/" >&2
   exit 2
 fi
 
-# --- arch normalize ---
+# --- arch normalize (uname -m → release/<arch>) ---
 _arch_raw="$(uname -m | tr '[:upper:]' '[:lower:]')"
 case "$_arch_raw" in
-  amd64|x64)              _arch="x86_64" ;;
-  x86_64)                 _arch="x86_64" ;;
-  i386|i486|i586|i686)    _arch="i686" ;;
-  arm64|aarch64)          _arch="aarch64" ;;
-  armv7l)                 _arch="armv7l" ;;
-  armv6l)                 _arch="armv6l" ;;
-  riscv64)                _arch="riscv64" ;;
-  ppc64le|powerpc64le)    _arch="ppc64le" ;;
-  s390x)                  _arch="s390x" ;;
-  *)                      _arch="$_arch_raw" ;;
+  amd64|x64)               _arch="x86_64" ;;
+  x86_64)                  _arch="x86_64" ;;
+  i386|i486|i586|i686)     _arch="i686"   ;;
+  arm64|aarch64)           _arch="aarch64";;
+  armv7l)                  _arch="armv7l" ;;
+  armv6l)                  _arch="armv6l" ;;
+  riscv64)                 _arch="riscv64";;
+  ppc64le|powerpc64le)     _arch="ppc64le";;
+  s390x)                   _arch="s390x"  ;;
+  *)                       _arch="$_arch_raw" ;;
 esac
 
-_gasket="$_release_dir/$_arch/man_in_grey_apply"
-_apply_args=()
-if [[ -x "$_gasket" ]]; then
-  _apply_args=(--apply-cmd "$_gasket")
+_gasket="${_release_dir}/${_arch}/man_in_grey_apply"
+if [[ ! -x "$_gasket" ]]; then
+  echo "error: gasket missing: ${_gasket}" >&2
+  echo "hint: from \$REPO_HOME/developer: run  compile  then  release" >&2
+  exit 2
 fi
 
-# --- run orchestrator ---
-exec python3 "$_py_entry" "${_apply_args[@]}" "$@"
+# --- run orchestrator (single code path via gasket) ---
+exec "${PYTHON:-python3}" "$_py_entry" --apply-cmd "$_gasket" "$@"
index e10314b..22853b6 100644 (file)
@@ -201,19 +201,16 @@ def _find_inner_py(repo_root: Path)-> Path|None:
   cand = repo_root/"release"/"python3"/"executor_inner.py"
   return cand if cand.is_file() else None
 
-def _apply_via_gasket(cbor_bytes: bytes ,apply_cmd: Path ,args)-> int:
-  cmd = [
-    str(apply_cmd)
-    ,"--plan" ,"-"   
-  ]
-  if args.phase_2_print:      cmd.append("--phase-2-print")
-  if args.phase_2_then_stop:  cmd.append("--phase-2-then-stop")
-  if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop")
-  if args.phase_2_sanity1_then_stop:   cmd.append("--phase-2-sanity1-then-stop")
-  if args.phase_2_validity_then_stop:  cmd.append("--phase-2-validity-then-stop")
-  if args.phase_2_sanity2_then_stop:   cmd.append("--phase-2-sanity2-then-stop")
-  proc = subprocess.run(cmd ,input=cbor_bytes)
-  return proc.returncode
+def _apply_via_gasket(cbor_bytes: bytes, apply_cmd: Path, args) -> int:
+    cmd = [str(apply_cmd), "--plan", "-"]  # <— tell gasket to read from stdin
+    if args.phase_2_print:      cmd.append("--phase-2-print")
+    if args.phase_2_then_stop:  cmd.append("--phase-2-then-stop")
+    if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop")
+    if args.phase_2_sanity1_then_stop:   cmd.append("--phase-2-sanity1-then-stop")
+    if args.phase_2_validity_then_stop:  cmd.append("--phase-2-validity-then-stop")
+    if args.phase_2_sanity2_then_stop:   cmd.append("--phase-2-sanity2-then-stop")
+    proc = subprocess.run(cmd, input=cbor_bytes)
+    return proc.returncode
 
 def _apply_via_inner_py(cbor_bytes: bytes ,inner_py: Path ,args)-> int:
   cmd = [
index 433b705..eb6bf5c 100644 (file)
@@ -158,6 +158,9 @@ def _safe_open_dir(dpath: str)-> int:
     os.close(fd) ; raise OSError("not a directory")
   return fd
 
+
+
+
 def check_sanity_2(journal: Journal)-> list[str]:
   errs: list[str] = []
   opened: dict[str ,int] = {}
@@ -174,14 +177,18 @@ def check_sanity_2(journal: Journal)-> list[str]:
       except Exception as e:
         errs.append(f"[{i}] cannot open destination dir: {d} ({e})")
 
-    # also warn on multiple writes to same (d,f) without displacement/delete
-    seen: set[tuple[str ,str]] = set()
-    for i ,cmd in enumerate(journal.command_list ,start=1):
+    # detect multiple writes to same target without a reset (displace/delete)
+    last_action: dict[tuple[str, str], str] = {}
+    for i, cmd in enumerate(journal.command_list, start=1):
       ad = cmd.arg_dict
-      key = (ad.get("write_file_dpath_str") ,ad.get("write_file_fname"))
-      if key in seen and cmd.name_str == "copy":
-        errs.append(f"[{i}] multiple writes to same target without prior displace/delete: {_dst_from(ad)}")
-      seen.add(key)
+      key = (ad.get("write_file_dpath_str"), ad.get("write_file_fname"))
+      op = cmd.name_str
+      if op == "copy":
+        if last_action.get(key) == "copy":
+          errs.append(f"[{i}] multiple writes to same target without prior displace/delete: {_dst_from(ad)}")
+        last_action[key] = "copy"
+      elif op in {"displace", "delete"}:
+        last_action[key] = op
 
   finally:
     for fd in opened.values():
@@ -378,56 +385,86 @@ def run_executor_inner(
 
 # --- main stays a thin arg wrapper ------------------------------------------
 
-def main(argv: list[str]|None=None)-> int:
+# --- plan input helpers -------------------------------------------------------
+
+def _read_fd_all(fd: int) -> bytes:
+  "Read all bytes from an already-open file descriptor without closing it."
+  chunks: list[bytes] = []
+  while True:
+    try:
+      b = os.read(fd, 65536)
+    except InterruptedError:
+      continue
+    if not b:
+      break
+    chunks.append(b)
+  return b"".join(chunks)
+
+def _read_plan_bytes_from_args(args) -> bytes:
+  """
+  Input priority:
+    1) --plan-fd <n>  (gasket path; do not close fd)
+    2) --plan -       (stdin)
+    3) --plan <file>  (read from filesystem)
+  """
+  if getattr(args, "plan_fd", -1) is not None and args.plan_fd >= 0:
+    return _read_fd_all(args.plan_fd)
+  if args.plan in ("", "-"):
+    return sys.stdin.buffer.read()
+  return Path(args.plan).read_bytes()
 
+def main(argv: list[str] | None = None) -> int:
   ap = argparse.ArgumentParser(
-    prog="executor_inner.py"
-    ,description="Man_In_Gray inner executor (decode → validate → apply)"
+    prog="executor_inner.py",
+    description="Man_In_Grey inner executor (decode → validate → apply)"
   )
-  ap.add_argument("--plan" ,required=True ,help="path to CBOR plan file")
-  ap.add_argument("--phase-2-print" ,action="store_true" ,help="print decoded journal")
-  ap.add_argument("--phase-2-then-stop" ,action="store_true" ,help="stop after print (no apply)")
-  ap.add_argument("--phase-2-wellformed-then-stop" ,action="store_true" ,help="stop after wellformed checks")
-  ap.add_argument("--phase-2-sanity1-then-stop"   ,action="store_true" ,help="stop after sanity-1 checks")
-  ap.add_argument("--phase-2-validity-then-stop"  ,action="store_true" ,help="stop after validity checks")
-  ap.add_argument("--phase-2-sanity2-then-stop"   ,action="store_true" ,help="stop after sanity-2 checks")
 
-  ap.add_argument("--plan" ,default="" ,help="path to CBOR plan file or '-' for stdin")
-  ap.add_argument("--plan-fd" ,type=int ,default=-1 ,help=argparse.SUPPRESS)
+  # Single --plan plus a hidden --plan-fd used by the gasket
+  ap.add_argument(
+    "--plan",
+    default="-",
+    help="path to CBOR plan file or '-' for stdin"
+  )
+  ap.add_argument(
+    "--plan-fd",
+    type=int,
+    default=-1,
+    help=argparse.SUPPRESS
+  )
+
+  # phase-2 gates (same semantics as before)
+  ap.add_argument("--phase-2-print", action="store_true", help="print decoded journal")
+  ap.add_argument("--phase-2-then-stop", action="store_true", help="stop after print (no apply)")
+  ap.add_argument("--phase-2-wellformed-then-stop", action="store_true", help="stop after wellformed checks")
+  ap.add_argument("--phase-2-sanity1-then-stop",   action="store_true", help="stop after sanity-1 checks")
+  ap.add_argument("--phase-2-validity-then-stop",  action="store_true", help="stop after validity checks")
+  ap.add_argument("--phase-2-sanity2-then-stop",   action="store_true", help="stop after sanity-2 checks")
 
   args = ap.parse_args(argv)
 
-  # load plan
+  # Read plan bytes from fd/stdin/file
   try:
-    if args.plan_fd >= 0:
-      import os as _os
-      data = _os.read(args.plan_fd ,1<<30)
-    elif args.plan == "-":
-      import sys as _sys
-      data = _sys.stdin.buffer.read()
-    elif args.plan:
-      data = Path(args.plan).read_bytes()
-    else:
-      print("error: either --plan <file|-> or --plan-fd <n> is required" ,file=sys.stderr)
-      return 2
+    data = _read_plan_bytes_from_args(args)
   except Exception as e:
-    print(f"error: failed to read plan: {e}" ,file=sys.stderr)
+    print(f"error: failed to read plan: {e}"file=sys.stderr)
     return 2
 
+  # Decode CBOR → Journal
   try:
     journal = _journal_from_cbor_bytes(data)
   except Exception as e:
-    print(f"error: failed to decode CBOR: {e}" ,file=sys.stderr)
+    print(f"error: failed to decode CBOR: {e}"file=sys.stderr)
     return 2
 
+  # Run the pipeline
   return executor_inner(
-    journal
-    ,phase_2_print=args.phase_2_print
-    ,phase_2_then_stop=args.phase_2_then_stop
-    ,phase_2_wellformed_then_stop=args.phase_2_wellformed_then_stop
-    ,phase_2_sanity1_then_stop=args.phase_2_sanity1_then_stop
-    ,phase_2_validity_then_stop=args.phase_2_validity_then_stop
-    ,phase_2_sanity2_then_stop=args.phase_2_sanity2_then_stop
+    journal,
+    phase_2_print=args.phase_2_print,
+    phase_2_then_stop=args.phase_2_then_stop,
+    phase_2_wellformed_then_stop=args.phase_2_wellformed_then_stop,
+    phase_2_sanity1_then_stop=args.phase_2_sanity1_then_stop,
+    phase_2_validity_then_stop=args.phase_2_validity_then_stop,
+    phase_2_sanity2_then_stop=args.phase_2_sanity2_then_stop,
   )
 
 if __name__ == "__main__":
diff --git a/document/todo.txt b/document/todo.txt
new file mode 100644 (file)
index 0000000..be8c0af
--- /dev/null
@@ -0,0 +1,6 @@
+
+2025-09-20 16:21:44Z [Man_In_Grey:tester]
+
+port executor_outer.py to C integrate it with apply.c
+
+add more tests.
index e10314b..22853b6 100755 (executable)
@@ -201,19 +201,16 @@ def _find_inner_py(repo_root: Path)-> Path|None:
   cand = repo_root/"release"/"python3"/"executor_inner.py"
   return cand if cand.is_file() else None
 
-def _apply_via_gasket(cbor_bytes: bytes ,apply_cmd: Path ,args)-> int:
-  cmd = [
-    str(apply_cmd)
-    ,"--plan" ,"-"   
-  ]
-  if args.phase_2_print:      cmd.append("--phase-2-print")
-  if args.phase_2_then_stop:  cmd.append("--phase-2-then-stop")
-  if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop")
-  if args.phase_2_sanity1_then_stop:   cmd.append("--phase-2-sanity1-then-stop")
-  if args.phase_2_validity_then_stop:  cmd.append("--phase-2-validity-then-stop")
-  if args.phase_2_sanity2_then_stop:   cmd.append("--phase-2-sanity2-then-stop")
-  proc = subprocess.run(cmd ,input=cbor_bytes)
-  return proc.returncode
+def _apply_via_gasket(cbor_bytes: bytes, apply_cmd: Path, args) -> int:
+    cmd = [str(apply_cmd), "--plan", "-"]  # <— tell gasket to read from stdin
+    if args.phase_2_print:      cmd.append("--phase-2-print")
+    if args.phase_2_then_stop:  cmd.append("--phase-2-then-stop")
+    if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop")
+    if args.phase_2_sanity1_then_stop:   cmd.append("--phase-2-sanity1-then-stop")
+    if args.phase_2_validity_then_stop:  cmd.append("--phase-2-validity-then-stop")
+    if args.phase_2_sanity2_then_stop:   cmd.append("--phase-2-sanity2-then-stop")
+    proc = subprocess.run(cmd, input=cbor_bytes)
+    return proc.returncode
 
 def _apply_via_inner_py(cbor_bytes: bytes ,inner_py: Path ,args)-> int:
   cmd = [
index 433b705..eb6bf5c 100755 (executable)
@@ -158,6 +158,9 @@ def _safe_open_dir(dpath: str)-> int:
     os.close(fd) ; raise OSError("not a directory")
   return fd
 
+
+
+
 def check_sanity_2(journal: Journal)-> list[str]:
   errs: list[str] = []
   opened: dict[str ,int] = {}
@@ -174,14 +177,18 @@ def check_sanity_2(journal: Journal)-> list[str]:
       except Exception as e:
         errs.append(f"[{i}] cannot open destination dir: {d} ({e})")
 
-    # also warn on multiple writes to same (d,f) without displacement/delete
-    seen: set[tuple[str ,str]] = set()
-    for i ,cmd in enumerate(journal.command_list ,start=1):
+    # detect multiple writes to same target without a reset (displace/delete)
+    last_action: dict[tuple[str, str], str] = {}
+    for i, cmd in enumerate(journal.command_list, start=1):
       ad = cmd.arg_dict
-      key = (ad.get("write_file_dpath_str") ,ad.get("write_file_fname"))
-      if key in seen and cmd.name_str == "copy":
-        errs.append(f"[{i}] multiple writes to same target without prior displace/delete: {_dst_from(ad)}")
-      seen.add(key)
+      key = (ad.get("write_file_dpath_str"), ad.get("write_file_fname"))
+      op = cmd.name_str
+      if op == "copy":
+        if last_action.get(key) == "copy":
+          errs.append(f"[{i}] multiple writes to same target without prior displace/delete: {_dst_from(ad)}")
+        last_action[key] = "copy"
+      elif op in {"displace", "delete"}:
+        last_action[key] = op
 
   finally:
     for fd in opened.values():
@@ -378,56 +385,86 @@ def run_executor_inner(
 
 # --- main stays a thin arg wrapper ------------------------------------------
 
-def main(argv: list[str]|None=None)-> int:
+# --- plan input helpers -------------------------------------------------------
+
+def _read_fd_all(fd: int) -> bytes:
+  "Read all bytes from an already-open file descriptor without closing it."
+  chunks: list[bytes] = []
+  while True:
+    try:
+      b = os.read(fd, 65536)
+    except InterruptedError:
+      continue
+    if not b:
+      break
+    chunks.append(b)
+  return b"".join(chunks)
+
+def _read_plan_bytes_from_args(args) -> bytes:
+  """
+  Input priority:
+    1) --plan-fd <n>  (gasket path; do not close fd)
+    2) --plan -       (stdin)
+    3) --plan <file>  (read from filesystem)
+  """
+  if getattr(args, "plan_fd", -1) is not None and args.plan_fd >= 0:
+    return _read_fd_all(args.plan_fd)
+  if args.plan in ("", "-"):
+    return sys.stdin.buffer.read()
+  return Path(args.plan).read_bytes()
 
+def main(argv: list[str] | None = None) -> int:
   ap = argparse.ArgumentParser(
-    prog="executor_inner.py"
-    ,description="Man_In_Gray inner executor (decode → validate → apply)"
+    prog="executor_inner.py",
+    description="Man_In_Grey inner executor (decode → validate → apply)"
   )
-  ap.add_argument("--plan" ,required=True ,help="path to CBOR plan file")
-  ap.add_argument("--phase-2-print" ,action="store_true" ,help="print decoded journal")
-  ap.add_argument("--phase-2-then-stop" ,action="store_true" ,help="stop after print (no apply)")
-  ap.add_argument("--phase-2-wellformed-then-stop" ,action="store_true" ,help="stop after wellformed checks")
-  ap.add_argument("--phase-2-sanity1-then-stop"   ,action="store_true" ,help="stop after sanity-1 checks")
-  ap.add_argument("--phase-2-validity-then-stop"  ,action="store_true" ,help="stop after validity checks")
-  ap.add_argument("--phase-2-sanity2-then-stop"   ,action="store_true" ,help="stop after sanity-2 checks")
 
-  ap.add_argument("--plan" ,default="" ,help="path to CBOR plan file or '-' for stdin")
-  ap.add_argument("--plan-fd" ,type=int ,default=-1 ,help=argparse.SUPPRESS)
+  # Single --plan plus a hidden --plan-fd used by the gasket
+  ap.add_argument(
+    "--plan",
+    default="-",
+    help="path to CBOR plan file or '-' for stdin"
+  )
+  ap.add_argument(
+    "--plan-fd",
+    type=int,
+    default=-1,
+    help=argparse.SUPPRESS
+  )
+
+  # phase-2 gates (same semantics as before)
+  ap.add_argument("--phase-2-print", action="store_true", help="print decoded journal")
+  ap.add_argument("--phase-2-then-stop", action="store_true", help="stop after print (no apply)")
+  ap.add_argument("--phase-2-wellformed-then-stop", action="store_true", help="stop after wellformed checks")
+  ap.add_argument("--phase-2-sanity1-then-stop",   action="store_true", help="stop after sanity-1 checks")
+  ap.add_argument("--phase-2-validity-then-stop",  action="store_true", help="stop after validity checks")
+  ap.add_argument("--phase-2-sanity2-then-stop",   action="store_true", help="stop after sanity-2 checks")
 
   args = ap.parse_args(argv)
 
-  # load plan
+  # Read plan bytes from fd/stdin/file
   try:
-    if args.plan_fd >= 0:
-      import os as _os
-      data = _os.read(args.plan_fd ,1<<30)
-    elif args.plan == "-":
-      import sys as _sys
-      data = _sys.stdin.buffer.read()
-    elif args.plan:
-      data = Path(args.plan).read_bytes()
-    else:
-      print("error: either --plan <file|-> or --plan-fd <n> is required" ,file=sys.stderr)
-      return 2
+    data = _read_plan_bytes_from_args(args)
   except Exception as e:
-    print(f"error: failed to read plan: {e}" ,file=sys.stderr)
+    print(f"error: failed to read plan: {e}"file=sys.stderr)
     return 2
 
+  # Decode CBOR → Journal
   try:
     journal = _journal_from_cbor_bytes(data)
   except Exception as e:
-    print(f"error: failed to decode CBOR: {e}" ,file=sys.stderr)
+    print(f"error: failed to decode CBOR: {e}"file=sys.stderr)
     return 2
 
+  # Run the pipeline
   return executor_inner(
-    journal
-    ,phase_2_print=args.phase_2_print
-    ,phase_2_then_stop=args.phase_2_then_stop
-    ,phase_2_wellformed_then_stop=args.phase_2_wellformed_then_stop
-    ,phase_2_sanity1_then_stop=args.phase_2_sanity1_then_stop
-    ,phase_2_validity_then_stop=args.phase_2_validity_then_stop
-    ,phase_2_sanity2_then_stop=args.phase_2_sanity2_then_stop
+    journal,
+    phase_2_print=args.phase_2_print,
+    phase_2_then_stop=args.phase_2_then_stop,
+    phase_2_wellformed_then_stop=args.phase_2_wellformed_then_stop,
+    phase_2_sanity1_then_stop=args.phase_2_sanity1_then_stop,
+    phase_2_validity_then_stop=args.phase_2_validity_then_stop,
+    phase_2_sanity2_then_stop=args.phase_2_sanity2_then_stop,
   )
 
 if __name__ == "__main__":
index d4180e3..2ad1606 100755 (executable)
@@ -1,57 +1,62 @@
 #!/usr/bin/env bash
-# Man_In_Grey — canonical entrypoint for Man_In_Grey
-# - Resolves repo root via this script’s location (…/release/shell/)
-# - Picks gasket at release/<arch>/man_in_grey_apply when present
-# - Falls back to Python inner executor
-# - Always invokes the Python orchestrator Man_In_Grey.py
-
+# man_in_grey — canonical entrypoint for Man_In_Grey
+# - Resolves repo → release dirs
+# - Requires the privileged gasket (single code path)
+# - Always invokes the Python orchestrator with --apply-cmd <gasket>
 set -euo pipefail
 
-# --- resolve paths ---
-_this="${BASH_SOURCE[0]}"
+# --- resolve this script’s absolute path (portable) ---
+_this="${BASH_SOURCE[0]:-$0}"
+
 if command -v realpath >/dev/null 2>&1; then
-  _this_abs="$(realpath "$_this")"
+  _this_abs="$(realpath -- "$_this")"
 else
-  _this_abs="$(readlink -f "$_this" 2>/dev/null || (cd "$(dirname "$_this")" && pwd -P)/"$(basename "$_this"))"
+  if _tmp="$(readlink -f -- "$_this" 2>/dev/null)"; then
+    _this_abs="${_tmp}"
+  else
+    _dir="$(cd "$(dirname -- "$_this")" && pwd -P)"
+    _base="$(basename -- "$_this")"
+    _this_abs="${_dir}/${_base}"
+  fi
 fi
 
-_shell_dir="$(cd "$(dirname "$_this_abs")" && pwd -P)"   # .../release/shell
-_release_dir="$(cd "$_shell_dir/.." && pwd -P)"         # .../release
-_repo_root="$(cd "$_release_dir/.." && pwd -P)"         # repo root
-
-_py_release="$_release_dir/python3"
-_py_dev="$_repo_root/developer/source"
-
-_py_entry=""
-if [[ -f "$_py_release/Man_In_Grey.py" ]]; then
-  _py_entry="$_py_release/Man_In_Grey.py"
-elif [[ -f "$_py_dev/Man_In_Grey.py" ]]; then
-  _py_entry="$_py_dev/Man_In_Grey.py"
+_shell_dir="$(cd "$(dirname -- "$_this_abs")" && pwd -P)"   # .../release/shell
+_release_dir="$(cd "${_shell_dir}/.." && pwd -P)"           # .../release
+_repo_root="$(cd "${_release_dir}/.." && pwd -P)"           # repo root
+
+# --- orchestrator path (prefer released copy) ---
+_py_release="${_release_dir}/python3/Man_In_Grey.py"
+_py_dev="${_repo_root}/developer/source/Man_In_Grey.py"
+if [[ -f "$_py_release" ]]; then
+  _py_entry="$_py_release"
+elif [[ -f "$_py_dev" ]]; then
+  _py_entry="$_py_dev"
 else
   echo "error: Man_In_Grey.py not found in release/python3/ or developer/source/" >&2
   exit 2
 fi
 
-# --- arch normalize ---
+# --- arch normalize (uname -m → release/<arch>) ---
 _arch_raw="$(uname -m | tr '[:upper:]' '[:lower:]')"
 case "$_arch_raw" in
-  amd64|x64)              _arch="x86_64" ;;
-  x86_64)                 _arch="x86_64" ;;
-  i386|i486|i586|i686)    _arch="i686" ;;
-  arm64|aarch64)          _arch="aarch64" ;;
-  armv7l)                 _arch="armv7l" ;;
-  armv6l)                 _arch="armv6l" ;;
-  riscv64)                _arch="riscv64" ;;
-  ppc64le|powerpc64le)    _arch="ppc64le" ;;
-  s390x)                  _arch="s390x" ;;
-  *)                      _arch="$_arch_raw" ;;
+  amd64|x64)               _arch="x86_64" ;;
+  x86_64)                  _arch="x86_64" ;;
+  i386|i486|i586|i686)     _arch="i686"   ;;
+  arm64|aarch64)           _arch="aarch64";;
+  armv7l)                  _arch="armv7l" ;;
+  armv6l)                  _arch="armv6l" ;;
+  riscv64)                 _arch="riscv64";;
+  ppc64le|powerpc64le)     _arch="ppc64le";;
+  s390x)                   _arch="s390x"  ;;
+  *)                       _arch="$_arch_raw" ;;
 esac
 
-_gasket="$_release_dir/$_arch/man_in_grey_apply"
-_apply_args=()
-if [[ -x "$_gasket" ]]; then
-  _apply_args=(--apply-cmd "$_gasket")
+_gasket="${_release_dir}/${_arch}/man_in_grey_apply"
+if [[ ! -x "$_gasket" ]]; then
+  echo "error: gasket missing: ${_gasket}" >&2
+  echo "hint: from \$REPO_HOME/developer: run  compile  then  release" >&2
+  exit 2
 fi
 
-# --- run orchestrator ---
-exec python3 "$_py_entry" "${_apply_args[@]}" "$@"
+# --- run orchestrator (single code path via gasket) ---
+exec "${PYTHON:-python3}" "$_py_entry" --apply-cmd "$_gasket" "$@"
diff --git a/tester/test_0/Man_In_Grey_input_acceptance.py b/tester/test_0/Man_In_Grey_input_acceptance.py
new file mode 100644 (file)
index 0000000..2608280
--- /dev/null
@@ -0,0 +1,26 @@
+# Man_In_Grey acceptance filter (default template)
+# Return True to include a config file ,False to skip it.
+# You receive a PlanProvenance object named `prov`.
+#
+# Common fields:
+#  prov.stage_root_dpath : Path
+#  prov.config_abs_fpath : Path
+#  prov.config_rel_fpath : Path
+#  prov.read_dir_dpath   : Path
+#  prov.read_fname       : str
+#
+# 1) Accept everything (default):
+# def accept(prov):
+#   return True
+#
+# 2) Only a namespace:
+# def accept(prov):
+#   return prov.config_rel_fpath.as_posix().startswith("dns/")
+#
+# 3) Exclude editor junk:
+# def accept(prov):
+#   r = prov.config_rel_fpath.as_posix()
+#   return not (r.endswith("~") or r.endswith(".swp"))
+#
+def accept(prov):
+  return True
diff --git a/tester/test_0/cat_stage b/tester/test_0/cat_stage
new file mode 100755 (executable)
index 0000000..4aa0ef6
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
+set -euo pipefail
+set -x
+
+find "$1" -type f -exec cat_named {} \;
+
diff --git a/tester/test_0/clean_out b/tester/test_0/clean_out
new file mode 100755 (executable)
index 0000000..5d6e333
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
+set -euo pipefail
+set -x
+
+find stage_test_0_out -type f -exec rm \-f {} \;
+
diff --git a/tester/test_0/ls_stage b/tester/test_0/ls_stage
new file mode 100755 (executable)
index 0000000..ea3750f
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
+set -euo pipefail
+set -x
+
+find "$1" -type f -print
+
index 6179ab1..4e8e3da 100755 (executable)
@@ -1,92 +1,7 @@
 #!/usr/bin/env bash
-set -euo pipefail
-
-# discover repo root from here
 script_afp=$(realpath "${BASH_SOURCE[0]}")
-TESTER_DIR="$(cd "$(dirname "$script_afp")/.." && pwd -P)"
-REPO_HOME="$(cd "$TESTER_DIR/.." && pwd -P)"
-
-ENTRY="$REL_SHELL/Man_In_Grey"
-GASKET="$REL_ARCH/man_in_grey_apply"
-
-# tester has a path to `release/shell`
-ENTRY="Man_In_Grey"
-
-# sanity
-[[ -x "$ENTRY" ]] || { echo "❌ missing entrypoint: $ENTRY (did you run developer/release?)" >&2; exit 2; }
-
-# ensure tester won’t hit privileged gasket refusal:
-ARCH_RAW=$(uname -m | tr '[:upper:]' '[:lower:]')
-case "$ARCH_RAW" in
-  amd64|x64) ARCH="x86_64" ;;
-  x86_64) ARCH="x86_64" ;;
-  i386|i486|i586|i686) ARCH="i686" ;;
-  arm64|aarch64) ARCH="aarch64" ;;
-  armv7l) ARCH="armv7l" ;;
-  armv6l) ARCH="armv6l" ;;
-  riscv64) ARCH="riscv64" ;;
-  ppc64le|powerpc64le) ARCH="ppc64le" ;;
-  s390x) ARCH="s390x" ;;
-  *) ARCH="$ARCH_RAW" ;;
-esac
-GASKET="$REPO_HOME/release/$ARCH/man_in_grey_apply"
-
-if [[ -x "$GASKET" && -u "$GASKET" ]]; then
-  echo "⚠️  Gasket is blessed (setuid-root) but tester is sudo-less."
-  echo "   Run:  sudo ./tool/unbless"
-  exit 1
-fi
-
-# fresh output dir
-rm -rf -- "$OUT"
-mkdir -p "$OUT"
 
-echo "▶️  Running Man_In_Grey on tester/$STAGE → $OUT"
-# Run planner → CBOR → apply (unprivileged). Default filter will be emitted in CWD if missing.
-( cd "$TESTER_DIR" && \
-  "$ENTRY" \
-    --stage "$STAGE" \
-    --phase-1-print \
-    --phase-2-print )
-
-echo "✅ Apply finished. Verifying…"
-
-fail=0
-
-# expected artifacts (from your sample stage)
-chk() {
-  local path="$1" desc="$2"
-  if [[ -f "$path" ]]; then
-    echo "  ✓ $desc ($path)"
-  else
-    echo "  ✗ $desc missing ($path)"; fail=1
-  fi
-}
-
-# files to expect
-chk "$OUT/unbound_conf"                "DNS base file"
-chk "$OUT/net/unbound.conf"            "DNS net override"
-chk "$OUT/web/nginx.conf"              "web nginx.conf"
-
-# content spot checks (adjust to your test content)
-if [[ -f "$OUT/net/unbound.conf" ]]; then
-  grep -q 'verbosity: 1' "$OUT/net/unbound.conf" \
-    && echo "  ✓ verbosity content OK" \
-    || { echo "  ✗ expected 'verbosity: 1' in net/unbound.conf"; fail=1; }
-fi
-
-if [[ -f "$OUT/web/nginx.conf" ]]; then
-  grep -q 'listen 8080' "$OUT/web/nginx.conf" \
-    && echo "  ✓ nginx listen OK" \
-    || { echo "  ✗ expected 'listen 8080' in web/nginx.conf"; fail=1; }
-fi
-
-# mode spot check (0444 example; chmod prints in octal differently across distros, use stat)
-if [[ -f "$OUT/unbound_conf" ]]; then
-  mode=$(stat -c '%a' "$OUT/unbound_conf" 2>/dev/null || stat -f '%Lp' "$OUT/unbound_conf")
-  [[ "$mode" == "444" ]] \
-    && echo "  ✓ mode unbound_conf is 0444" \
-    || { echo "  ⚠︎ mode unbound_conf is $mode (expected 444)"; :; }
-fi
+set -euo pipefail
+set -x
 
-[[ $fail -eq 0 ]] && echo "🎉 test_0 PASS" || { echo "❌ test_0 FAIL"; exit 1; }
+$RELEASE_SHELL/Man_In_Grey --stage stage_test_0
diff --git a/tester/test_0/run_check b/tester/test_0/run_check
new file mode 100755 (executable)
index 0000000..4b02ea6
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
+set -euo pipefail
+set -x
+
+./clean_out
+
+echo "first run"
+./run
+diff -qr stage_test_0_out stage_test_0_out_expected_0
+
+echo "second run"
+./run
+diff -qr stage_test_0_out stage_test_0_out_expected_1
diff --git a/tester/test_0/run_check.transcript b/tester/test_0/run_check.transcript
new file mode 100644 (file)
index 0000000..08b3fe9
--- /dev/null
@@ -0,0 +1,38 @@
+# due to file dates, the second check will find differences in file date pairs
+#
+# pass 1
+# Yes—this is a clean PASS ✅
+# started clean,
+# ran Man_In_Grey once → all three outputs appeared with expected content,
+# snapshotted as stage_test_0_out_expected_0.
+# 
+# pass 2
+# Yes—this is a clean PASS ✅
+# web/nginx.conf and unbound_conf were displaced on the 2nd run → timestamped backups created, new files written.
+#
+# net/unbound.conf shows no backup, which is correct because that config does delete then copy (so overwrite without keeping a prior version). If you want a backup there too, switch that entry from delete→displace.
+
+
+2025-09-20 16:12:28Z [Man_In_Grey:tester] Thomas-developer@StanleyPark
+§/home/Thomas/subu_data/developer/project/active/Man_In_Grey/tester/test_0§
+> ./run_check 
++ ./clean_out
++ find stage_test_0_out -type f -exec rm -f '{}' ';'
++ echo 'first run'
+first run
++ ./run
++ /home/Thomas/subu_data/developer/project/Linux/Man_In_Grey/release/shell/Man_In_Grey --stage stage_test_0
++ diff -qr stage_test_0_out stage_test_0_out_expected_0
++ echo 'second run'
+second run
++ ./run
++ /home/Thomas/subu_data/developer/project/Linux/Man_In_Grey/release/shell/Man_In_Grey --stage stage_test_0
++ diff -qr stage_test_0_out stage_test_0_out_expected_1
+Only in stage_test_0_out_expected_1: unbound_conf.20250920T160648Z
+Only in stage_test_0_out: unbound_conf.20250920T161232Z
+Only in stage_test_0_out_expected_1/web: nginx.conf.20250920T160648Z
+Only in stage_test_0_out/web: nginx.conf.20250920T161233Z
+
+2025-09-20 16:12:33Z [Man_In_Grey:tester] Thomas-developer@StanleyPark
+§/home/Thomas/subu_data/developer/project/active/Man_In_Grey/tester/test_0§
+> 
diff --git a/tester/test_0/stage_test_0_out/.githolder b/tester/test_0/stage_test_0_out/.githolder
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tester/test_0/stage_test_0_out/net/unbound.conf b/tester/test_0/stage_test_0_out/net/unbound.conf
new file mode 100644 (file)
index 0000000..b044194
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  verbosity: 1
diff --git a/tester/test_0/stage_test_0_out/unbound_conf b/tester/test_0/stage_test_0_out/unbound_conf
new file mode 100644 (file)
index 0000000..d52f868
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  do-ip6: no
diff --git a/tester/test_0/stage_test_0_out/unbound_conf.20250920T161232Z b/tester/test_0/stage_test_0_out/unbound_conf.20250920T161232Z
new file mode 100644 (file)
index 0000000..d52f868
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  do-ip6: no
diff --git a/tester/test_0/stage_test_0_out/web/nginx.conf b/tester/test_0/stage_test_0_out/web/nginx.conf
new file mode 100644 (file)
index 0000000..f48355c
--- /dev/null
@@ -0,0 +1,2 @@
+events {}
+http { server { listen 8080; } }
diff --git a/tester/test_0/stage_test_0_out/web/nginx.conf.20250920T161233Z b/tester/test_0/stage_test_0_out/web/nginx.conf.20250920T161233Z
new file mode 100644 (file)
index 0000000..f48355c
--- /dev/null
@@ -0,0 +1,2 @@
+events {}
+http { server { listen 8080; } }
diff --git a/tester/test_0/stage_test_0_out_expected_0/net/unbound.conf b/tester/test_0/stage_test_0_out_expected_0/net/unbound.conf
new file mode 100644 (file)
index 0000000..b044194
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  verbosity: 1
diff --git a/tester/test_0/stage_test_0_out_expected_0/unbound_conf b/tester/test_0/stage_test_0_out_expected_0/unbound_conf
new file mode 100644 (file)
index 0000000..d52f868
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  do-ip6: no
diff --git a/tester/test_0/stage_test_0_out_expected_0/web/nginx.conf b/tester/test_0/stage_test_0_out_expected_0/web/nginx.conf
new file mode 100644 (file)
index 0000000..f48355c
--- /dev/null
@@ -0,0 +1,2 @@
+events {}
+http { server { listen 8080; } }
diff --git a/tester/test_0/stage_test_0_out_expected_1/net/unbound.conf b/tester/test_0/stage_test_0_out_expected_1/net/unbound.conf
new file mode 100644 (file)
index 0000000..b044194
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  verbosity: 1
diff --git a/tester/test_0/stage_test_0_out_expected_1/unbound_conf b/tester/test_0/stage_test_0_out_expected_1/unbound_conf
new file mode 100644 (file)
index 0000000..d52f868
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  do-ip6: no
diff --git a/tester/test_0/stage_test_0_out_expected_1/unbound_conf.20250920T160648Z b/tester/test_0/stage_test_0_out_expected_1/unbound_conf.20250920T160648Z
new file mode 100644 (file)
index 0000000..d52f868
--- /dev/null
@@ -0,0 +1,2 @@
+server:
+  do-ip6: no
diff --git a/tester/test_0/stage_test_0_out_expected_1/web/nginx.conf b/tester/test_0/stage_test_0_out_expected_1/web/nginx.conf
new file mode 100644 (file)
index 0000000..f48355c
--- /dev/null
@@ -0,0 +1,2 @@
+events {}
+http { server { listen 8080; } }
diff --git a/tester/test_0/stage_test_0_out_expected_1/web/nginx.conf.20250920T160648Z b/tester/test_0/stage_test_0_out_expected_1/web/nginx.conf.20250920T160648Z
new file mode 100644 (file)
index 0000000..f48355c
--- /dev/null
@@ -0,0 +1,2 @@
+events {}
+http { server { listen 8080; } }
index de541e8..a6f9a98 100644 (file)
@@ -22,7 +22,7 @@ case "$_arch_raw" in
 esac
 export ARCH="$_arch"
 
-# Handy convenience paths (optional)
-export REL_SHELL="$RELEASE/shell"
-export REL_PY="$RELEASE/python3"
-export REL_ARCH="$RELEASE/$ARCH"
+# important release directories the main program call is in RELEASE_SHELL
+export RELEASE_SHELL="$RELEASE/shell"
+export RELEASE_PY="$RELEASE/python3"
+export RELEASE_ARCH="$RELEASE/$ARCH"
index 4725c04..2cdede9 100644 (file)
@@ -58,6 +58,13 @@ Ensure the following:
 
    The binary should exist and report a working Python interpreter when run.
 
+6. Install cbor2
+
+   which python3
+   python3 -m pip install --upgrade pip
+   python3 -m pip install cbor2
+
+
 * Notes
 
 - The virtual environment is deliberately named =Python=, not =venv=, to reflect its role as a shared system component.