From 784b4fd404a4330a2984948c469cad0c07ce5e4a Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Wed, 19 Nov 2025 08:24:27 +0000 Subject: [PATCH] . --- tool/skel.tgz | Bin 0 -> 35579 bytes tool/skeleton/CLI.py | 152 +++++++++++-- tool/skeleton/command.py | 460 +++++++++++++++++++++++++++++++++++++++ tool/skeleton/doc.py | 94 +++++++- 4 files changed, 672 insertions(+), 34 deletions(-) create mode 100644 tool/skel.tgz create mode 100644 tool/skeleton/command.py diff --git a/tool/skel.tgz b/tool/skel.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7e2bf1ac079b9e66b8b5ddb9a7fe4e38d7e431f6 GIT binary patch literal 35579 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

F~#fW=2 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 WF~>0N|0mO-6mDUtMJ1IY4b^{Yr09tN literal 0 HcmV?d00001 diff --git a/tool/skeleton/CLI.py b/tool/skeleton/CLI.py index b62d3a8..46e810b 100755 --- a/tool/skeleton/CLI.py +++ b/tool/skeleton/CLI.py @@ -2,7 +2,7 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -CLI.py - command classification and debug printer +CLI.py - Harmony skeleton checker Grammar (informal): @@ -25,23 +25,34 @@ At runtime, argv commands are classified into four lists: 3. has_other_list 4. unclassified_list -If the meta debug set contains the tag "Command", these four lists +If the meta debug set contains the tag "print_command_lists", these four lists are printed. If 'environment' appears in no_other_list, the meta.printenv() helper is invoked to print the environment. + +For commands we compare: + + A = Harmony skeleton tree_dict + B = project tree_dict (path is the last argv token when any + is present before it). """ from __future__ import annotations -import meta +import os import sys from typing import Sequence + +import command import doc +import Harmony +import meta +import skeleton meta.debug_set("print_command_lists") -# print_command_lists tag sets (classification universe) +# Command tag sets (classification universe) HELP_COMMANDS: set[str] = set([ "version" ,"help" @@ -105,29 +116,54 @@ def CLI(argv: Sequence[str] | None = None) -> int: Responsibilities: 1. Accept argv (or sys.argv[1:] by default). - 2. Classify each argument using command_type(). + 2. Classify arguments using command_type(), with the last argument + treated specially to avoid aliasing. 3. Invoke behaviors implied by the commands. 4. Return integer status code. - Behavior (current): - 1. Build four lists, in argv order: - - help_list - - no_other_list - - has_other_list - - unclassified_list - 2. If "print_command_lists" is enabled in meta's debug set, print those lists. - 3. If 'environment' is present in no_other_list, call meta.printenv(). - 4. If any help commands appear, handle them and return 1. + Argument interpretation: + + Let argv = [a0, a1, ..., aN-1]. + + - If N == 0: + no commands; nothing to do. + + - If N >= 1: + * Classify a0..aN-2. + - If any are UnClassified -> error. + + * If any appear in a0..aN-2: + - aN-1 is treated as path (B_root), not classified. + + * If no appear in a0..aN-2: + - Classify aN-1: + - If UnClassified -> error (unknown command). + - If HasOther -> error (other path not specified). + - Else -> added to Help / NoOther lists. """ if argv is None: argv = sys.argv[1:] + # No arguments: print usage and exit with status 1. + if len(argv) == 0: + doc.print_usage() + return 1 + + # No arguments: nothing to do (could later decide to print usage). + if len(argv) == 0: + return 0 + + # Split into head (all but last) and last argument + head = argv[:-1] + last = argv[-1] + help_list: list[str] = [] no_other_list: list[str] = [] has_other_list: list[str] = [] unclassified_list: list[str] = [] - for arg in argv: + # 1. Classify head tokens + for arg in head: ct = command_type(arg) if ct == "Help": @@ -139,6 +175,37 @@ def CLI(argv: Sequence[str] | None = None) -> int: else: unclassified_list.append(arg) + # Any unclassified in the head is an error + if len(unclassified_list) > 0: + first_bad = unclassified_list[0] + print(f"Unrecognized command: {first_bad}") + return 5 + + head_has_other = (len(has_other_list) > 0) + + B_root: str | None = None + + if head_has_other: + # 2A. Any in head -> last arg is always path. + B_root = os.path.abspath(last) + else: + # 2B. No in head -> classify last. + ct = command_type(last) + + if ct == "UnClassified": + print(f"Unrecognized command: {last}") + return 5 + + if ct == "HasOther": + print("Other path not specified for has_other command(s).") + return 6 + + if ct == "Help": + help_list.append(last) + elif ct == "NoOther": + no_other_list.append(last) + # ct cannot be HasOther here due to earlier check. + if meta.debug_has("print_command_lists"): print_command_lists( help_list @@ -147,6 +214,7 @@ def CLI(argv: Sequence[str] | None = None) -> int: ,unclassified_list ) + # Help handling if len(help_list) > 0: if "version" in help_list: meta.version_print() @@ -156,21 +224,59 @@ def CLI(argv: Sequence[str] | None = None) -> int: doc.print_help() return 1 -# status,Harmony_root = skeleton.where_is_Harmony() -# if status == 'different': -# print("Seems we are not running in the Harmony project, will exit.") -# return 2 -# if status == 'not-found': -# print("Harmony project not found, normally this command is run from with Harmony.") -# return 3 - ret_val = 0 + + # No-other commands (environment, etc.) if "environment" in no_other_list: env_status = meta.printenv() if env_status != 0: ret_val = env_status + # If we still have no has_other commands, we are done. + # (Example: just "environment", or just "help/usage".) + if len(has_other_list) == 0: + return ret_val + + # At this point we know: + # - has_other_list is non-empty + # - B_root must have been set (head_has_other was True) + if B_root is None: + print("Internal error: B_root not set despite has_other commands.") + return 7 + + if not os.path.isdir(B_root): + print(f"Other project path is not a directory: {B_root}") + return 4 + + # Determine Harmony root (A_root) + status, A_root = Harmony.where() + + if status == "not-found": + print("Harmony project not found; normally this command is run from within Harmony.") + return 3 + + if status == "different": + print("Seems we are not running in the Harmony project, will exit.") + return 2 + + # Build tree_dicts for A (Harmony) and B (other project) + A_tree = skeleton.tree_dict_make(A_root, None) + B_tree = skeleton.tree_dict_make(B_root, None) + + # Dispatch the commands + cmd_status = command.dispatch( + has_other_list + ,A_tree + ,B_tree + ,A_root + ,B_root + ) + + if cmd_status != 0: + ret_val = cmd_status + return ret_val + if __name__ == "__main__": raise SystemExit(CLI()) diff --git a/tool/skeleton/command.py b/tool/skeleton/command.py new file mode 100644 index 0000000..47520b1 --- /dev/null +++ b/tool/skeleton/command.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +""" +command.py - high-level dispatch for Harmony check commands + +Commands (semantics): + + structure: + - Differences in directory structure: directories present in A but + not present as directories in B. + + import: + - Shell copy commands to copy: + * nodes in B that are newer than A (same relative path), and + * nodes in B that do not exist in A at all. + Direction: B → A + + export: + - Shell copy commands to copy: + * nodes in A that are newer than B, and + * nodes in A that do not exist in B. + Direction: A → B + (Uses the "older" list: B entries older than A → copy A→B.) + + suspicious: + - Nodes in B that fall "in between" the Harmony skeleton topology: + under some A directory, but not under any A leaf directory. + (tree_dict_in_between_and_below(A,B).in_between) + + addendum: + - Nodes in B that fall "below" Harmony leaf directories: + added work in appropriate extension locations. + (tree_dict_in_between_and_below(A,B).below) + + all: + - Runs structure, import, export, suspicious, and addendum. +""" + +from __future__ import annotations + +import meta +import os +from typing import Any, Dict, List, Tuple +import skeleton + + +TreeDict = Dict[str, Dict[str, Any]] + +def build_import_commands( + A_tree: Dict[str, Dict[str, Any]] + ,B_tree: Dict[str, Dict[str, Any]] + ,A_root: str + ,B_root: str +) -> Tuple[List[str], List[str]]: + """ + Compute shell commands to update A from B. + + Returns: + (mkdir_cmds, cp_cmds) + + Semantics: + - mkdir_cmds: + Directories that are directories in B, but are either missing + from A or not directories in A. + We *only* ever create dirs that are missing or wrong-type on A. + + - cp_cmds: + Files (and optionally other non-directory nodes) where: + * the path does not exist in A, OR + * the node in A is not a file, OR + * the B copy is newer than A (mtime comparison). + """ + mkdir_cmds: List[str] = [] + cp_cmds: List[str] = [] + + for rel_path, b_info in B_tree.items(): + node_type = b_info.get("node_type") + + # Directories: candidate for mkdir on A if missing or wrong type. + if node_type == "directory": + a_info = A_tree.get(rel_path) + if a_info is None or a_info.get("node_type") != "directory": + # Missing or not a directory on A: mkdir -p + target_dir = os.path.join(A_root, rel_path) if rel_path else A_root + mkdir_cmds.append(f"mkdir -p '{target_dir}'") + continue + + # Files / other nodes: candidate for cp from B -> A + b_mtime = b_info.get("mtime") + a_info = A_tree.get(rel_path) + + need_copy = False + + if a_info is None: + # B-only + need_copy = True + else: + a_type = a_info.get("node_type") + if a_type != "file": + # A has non-file, B has file/other: prefer B’s version + need_copy = True + else: + # Both are files: compare mtime + a_mtime = a_info.get("mtime") + if isinstance(a_mtime, (int, float)) and isinstance(b_mtime, (int, float)): + if b_mtime > a_mtime: + need_copy = True + + if need_copy: + src = os.path.join(B_root, rel_path) if rel_path else B_root + dst = A_root # cp --parents will build the path under this root + cp_cmds.append( + f"cp --parents -a '{src}' '{dst}/'" + ) + + return mkdir_cmds, cp_cmds + +def shell_quote( + s: str +) -> str: + """ + Minimal single-quote shell quoting. + """ + return "'" + s.replace("'", "'\"'\"'") + "'" + + +def _print_header( + title: str +) -> None: + print() + print(f"== {title} ==") + + +# ---------------------------------------------------------------------- +# structure: directories in A that are missing / non-directories in B +# ---------------------------------------------------------------------- +def cmd_structure( + A: TreeDict + ,B: TreeDict +) -> int: + """ + structure: differences in directory structure, directories in A - B. + + We include any path where: + - A[path].node_type == 'directory', and + - either path not in B, or B[path].node_type != 'directory'. + """ + structural: TreeDict = {} + + for path, info_A in A.items(): + if info_A.get("node_type") != "directory": + continue + + info_B = B.get(path) + if info_B is None or info_B.get("node_type") != "directory": + structural[path] = info_A + + if not structural: + _print_header("structure") + print("No structural directory differences (A - B).") + return 0 + + _print_header("structure: directories in A not in B") + skeleton.tree_dict_print(structural) + return 0 + + +# ---------------------------------------------------------------------- +# import: copy newer / A-missing nodes from B → A +# ---------------------------------------------------------------------- +def _keys_only_in_B( + A: TreeDict + ,B: TreeDict +) -> Iterable[str]: + keys_A = set(A.keys()) + for k in B.keys(): + if k not in keys_A: + yield k + + +def cmd_import( + A_tree: Dict[str, Dict[str, Any]] + ,B_tree: Dict[str, Dict[str, Any]] + ,A_root: str + ,B_root: str +) -> int: + """ + import: show directory creation and copy commands B -> A. + """ + mkdir_cmds, cp_cmds = build_import_commands(A_tree, B_tree, A_root, B_root) + + print("== import: copy from B -> A ==") + print(f"# A root: {A_root}") + print(f"# B root: {B_root}") + print("#") + + print("# Directories to create in A (mkdir -p):") + if mkdir_cmds: + for line in mkdir_cmds: + print(line) + else: + print("# (none)") + print("#") + + print("# Files to copy from B -> A (cp --parents -a):") + if cp_cmds: + for line in cp_cmds: + print(line) + else: + print("# (none)") + + return 0 + + +def cmd_import( + A: TreeDict + ,B: TreeDict + ,A_root: str + ,B_root: str +) -> int: + """ + import: B → A + + - Newer nodes in B than A (same path): tree_dict_newer(A,B). + - Nodes present only in B (not in A). + - Only file nodes are turned into copy commands. + + Output: shell 'cp' commands using GNU 'cp --parents -a'. + """ + newer_B = skeleton.tree_dict_newer(A, B) + only_in_B_paths = list(_keys_only_in_B(A, B)) + + # Collect unique file paths to copy from B to A + paths: List[str] = [] + + for k in newer_B.keys(): + if B.get(k, {}).get("node_type") == "file": + paths.append(k) + + for k in only_in_B_paths: + if B.get(k, {}).get("node_type") == "file": + paths.append(k) + + # Deduplicate while preserving order + seen = set() + unique_paths: List[str] = [] + for p in paths: + if p in seen: + continue + seen.add(p) + unique_paths.append(p) + + _print_header("import: copy from B → A") + + if not unique_paths: + print("# No file nodes in B to import into A.") + return 0 + + print(f"# A root: {A_root}") + print(f"# B root: {B_root}") + print("# Copy newer and B-only files from B into A:") + for rel in unique_paths: + src = os.path.join(B_root, rel) + cmd = ( + f"cp --parents -a {shell_quote(src)} " + f"{shell_quote(A_root)}/" + ) + print(cmd) + + return 0 + + +# ---------------------------------------------------------------------- +# export: copy newer / B-missing nodes from A → B +# ---------------------------------------------------------------------- +def _keys_only_in_A( + A: TreeDict + ,B: TreeDict +) -> Iterable[str]: + keys_B = set(B.keys()) + for k in A.keys(): + if k not in keys_B: + yield k + + +def cmd_export( + A: TreeDict + ,B: TreeDict + ,A_root: str + ,B_root: str +) -> int: + """ + export: A → B + + - Nodes in B that are older than A (same path): + tree_dict_older(A,B) -> keys of interest. + For these keys, we copy from A_root/path to B_root/path. + + - Nodes present only in A (not in B). + + Only file nodes are turned into copy commands. + """ + older_B = skeleton.tree_dict_older(A, B) + only_in_A_paths = list(_keys_only_in_A(A, B)) + + paths: List[str] = [] + + for k in older_B.keys(): + if A.get(k, {}).get("node_type") == "file": + paths.append(k) + + for k in only_in_A_paths: + if A.get(k, {}).get("node_type") == "file": + paths.append(k) + + seen = set() + unique_paths: List[str] = [] + for p in paths: + if p in seen: + continue + seen.add(p) + unique_paths.append(p) + + _print_header("export: copy from A → B") + + if not unique_paths: + print("# No file nodes in A to export into B.") + return 0 + + print(f"# A root: {A_root}") + print(f"# B root: {B_root}") + print("# Copy newer and A-only files from A into B:") + for rel in unique_paths: + src = os.path.join(A_root, rel) + cmd = ( + f"cp --parents -a {shell_quote(src)} " + f"{shell_quote(B_root)}/" + ) + print(cmd) + + return 0 + + +# ---------------------------------------------------------------------- +# suspicious / addendum via in_between_and_below +# ---------------------------------------------------------------------- +def cmd_suspicious( + A: TreeDict + ,B: TreeDict +) -> int: + """ + suspicious: nodes in B that fall 'in between' the Harmony skeleton, + not under leaf directories. + + Uses tree_dict_in_between_and_below(A,B) and prints the 'in_between' + dictionary. + """ + in_between, _below = skeleton.tree_dict_in_between_and_below(A, B) + + _print_header("suspicious: nodes in-between Harmony leaves") + + if not in_between: + print("No suspicious nodes found in B (relative to A).") + return 0 + + skeleton.tree_dict_print(in_between) + return 0 + + +def cmd_addendum( + A: TreeDict + ,B: TreeDict +) -> int: + """ + addendum: nodes in B that fall 'below' Harmony leaf directories. + + These represent work added in proper extension points. + Uses the 'below' part from tree_dict_in_between_and_below(A,B). + """ + _in_between, below = skeleton.tree_dict_in_between_and_below(A, B) + + _print_header("addendum: nodes added under Harmony leaves") + + if not below: + print("No addendum nodes found in B (relative to A).") + return 0 + + skeleton.tree_dict_print(below) + return 0 + + +# ---------------------------------------------------------------------- +# Top-level dispatcher +# ---------------------------------------------------------------------- +def dispatch( + has_other_list: List[str] + ,A: TreeDict + ,B: TreeDict + ,A_root: str + ,B_root: str +) -> int: + """ + Dispatch commands. + + has_other_list: + List of command tokens (subset of: + 'structure', 'import', 'export', 'suspicious', 'addendum', 'all'). + + A, B: + tree_dicts for Harmony skeleton (A) and project (B). + + A_root, B_root: + Root paths corresponding to A and B (for copy commands). + """ + # Normalize commands + cmds = set(has_other_list) + + if "all" in cmds: + cmds.update([ + "structure" + ,"import" + ,"export" + ,"suspicious" + ,"addendum" + ]) + + # Preserve a deterministic run order + ordered = [ + "structure" + ,"import" + ,"export" + ,"suspicious" + ,"addendum" + ] + + status = 0 + + for name in ordered: + if name not in cmds: + continue + + if name == "structure": + rc = cmd_structure(A, B) + elif name == "import": + rc = cmd_import(A, B, A_root, B_root) + elif name == "export": + rc = cmd_export(A, B, A_root, B_root) + elif name == "suspicious": + rc = cmd_suspicious(A, B) + elif name == "addendum": + rc = cmd_addendum(A, B) + else: + # Unknown has_other token; ignore for now, could log later. + rc = 0 + + if rc != 0: + status = rc + + return status diff --git a/tool/skeleton/doc.py b/tool/skeleton/doc.py index d822a02..a8d5351 100644 --- a/tool/skeleton/doc.py +++ b/tool/skeleton/doc.py @@ -52,23 +52,95 @@ Where: def _help_text(prog: str) -> str: return f"""\ -{prog} — Harmony skeleton integrity and metadata checker +{prog} - Harmony skeleton integrity and metadata checker -For now: - This is a placeholder help message. +Syntax: + {prog} * [] - The tool accepts one or more tokens and an optional - argument. Each is classified as one of: +Where: + :: path + :: | | - - (version, help, usage) - - (environment) - - (structure, import, export, suspicious, addendum, all) + :: version | help | usage + :: environment + :: structure | import | export | suspicious | addendum | all -Detailed behavior for each command will be documented here as the -implementation is completed. +Argument rules (informal): + 1. commands are processed first, and then the program returns. + Hence if any help commands are present, the remaining commands + are ignored. + + 2. We assume {prog} is run within the Harmony skeleton, or a skeleton + derived from it. This is the 'default skeleton', or more simply, 'A'. + + 3. The path is the directory of a project that is assumed to + be built upon the default skeleton. This second project root is + referred to as 'B'. + + 4. If none of the commands require an path argument, then it + should not be given. Otherwise it is required. A command that + requires an path argument is called a command. + + 5. Implementation detail: all arguments except the last are first + treated as commands. If any of those are , the last + argument is interpreted as the path. If no + appears before the last argument, the last argument is treated as + another command. + +Roots: + A = Skeleton project root (auto-detected). Currently this is the + Harmony skeleton, but {prog} is not limited to Harmony. + + B = project root (path argument when required). + +{prog} is used to ask questions about how has changed relative +to the current default skeleton. Changes may come from edits to the +skeleton itself, edits to skeleton files in , or files and +directories added to . Stated briefly, {prog} compares A with B. +Conceptually, A and B are any two non-overlapping directory trees. + +Command semantics: + structure + - Report directory-structure differences: + directories present in A that are missing in B or not directories + in B. + - Output: a table of such directories. + + import + - Suggest shell copy commands to update A from B: + * files in B that are newer than A at the same relative path + * files that exist in B but not in A + - Direction: B -> A + - Output: 'cp --parents -a' commands (to be reviewed/edited before use). + + export + - Suggest shell copy commands to update B from A: + * files where the A copy is newer than B at the same path + * files that exist in A but not in B + - Direction: A -> B + - Output: 'cp --parents -a' commands (to be reviewed/edited before use). + + suspicious + - Report nodes in B that lie "in between" the Harmony skeleton: + under a directory present in A, but not under any leaf directory + in A. + - Intended to highlight questionable placements that may indicate + misuse of the skeleton or candidates for new skeleton structure. + + addendum + - Report nodes in B that lie "below" Harmony leaf directories: + work added in the intended extension points (tools, tests, etc.). + - Intended to show project-specific additions made in proper places. + + all + - Run: structure, import, export, suspicious, addendum (in that order). + +Notes: + - Directory and file listings respect a simplified .gitignore model + plus some always-ignored patterns (such as '.git' directories). + - Timestamps are formatted via the Z helper in UTC (ISO 8601). """ - def print_usage( stream: TextIO | None = None ) -> None: -- 2.20.1