From 970af177a37fc694a109093c9d6db7d41c31fb59 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Thu, 20 Nov 2025 04:12:43 +0000 Subject: [PATCH] cleanup --- tool/deprecated/skeleton_check | 187 ---------------- tool/deprecated/skeleton_commands.py | 249 --------------------- tool/deprecated/skeleton_compare.py | 76 ------- tool/deprecated/skeleton_config.py | 32 --- tool/deprecated/skeleton_diff_docs.py | 298 -------------------------- tool/deprecated/skeleton_paths.py | 170 --------------- tool/skel.tgz | Bin 35579 -> 0 bytes 7 files changed, 1012 deletions(-) delete mode 100755 tool/deprecated/skeleton_check delete mode 100644 tool/deprecated/skeleton_commands.py delete mode 100644 tool/deprecated/skeleton_compare.py delete mode 100644 tool/deprecated/skeleton_config.py delete mode 100644 tool/deprecated/skeleton_diff_docs.py delete mode 100644 tool/deprecated/skeleton_paths.py delete mode 100644 tool/skel.tgz diff --git a/tool/deprecated/skeleton_check b/tool/deprecated/skeleton_check deleted file mode 100755 index 7051acb..0000000 --- a/tool/deprecated/skeleton_check +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- - -""" -skeleton_check — CLI front-end for skeleton_diff -""" - -import sys -from pathlib import Path -from typing import List - -from skeleton_commands import ( - work_environment, - work_structure, - work_import, - work_export, - work_suspicious, - work_addendum, - work_version, - work_usage, -) -from skeleton_diff_docs import work_help - - -def CLI() -> int: - args_list = sys.argv[1:] - - program_name = Path(sys.argv[0]).name or "skeleton_check" - - if not args_list: - work_usage(program_name) - return 1 - - # 1. Global dominating commands: usage, help, version - global_dom_commands_set = { - "usage", - "help", - "version", - } - - for token in args_list: - if token in global_dom_commands_set: - if token == "usage": - work_usage(program_name) - elif token == "help": - work_help() - elif token == "version": - work_version() - return 0 - - # 2. Commands that never require a project, and those that do - commands_no_other_set = { - "environment", - } - - commands_require_other_set = { - "structure", - "import", - "export", - "suspicious", - "addendum", - "all", - } - - all_commands_set = commands_no_other_set | commands_require_other_set - - commands_list: List[str] = [] - other_root_path: Path | None = None - project_needed_flag = False - earliest_requires_index: int | None = None - - n_args = len(args_list) - last_index = n_args - 1 - - for index, token in enumerate(args_list): - if token in all_commands_set: - # If we already saw a project-requiring command earlier, and this is - # the last token, interpret it as the project path instead of a command. - if project_needed_flag and index == last_index and earliest_requires_index is not None: - other_root_path = Path(token) - break - - # Normal command - commands_list.append(token) - if token in commands_require_other_set and earliest_requires_index is None: - earliest_requires_index = index - project_needed_flag = True - - else: - # Not a known command: may be the project path, but only if a command - # that requires a project has already been seen and this is the last arg. - if project_needed_flag and index == last_index and earliest_requires_index is not None: - other_root_path = Path(token) - break - - print(f"ERROR: unknown command '{token}'.", file=sys.stderr) - work_usage(program_name) - return 1 - - # 3. Post-parse checks - if project_needed_flag: - # We saw at least one command that requires . - if other_root_path is None: - # First project-requiring command was also the last argument: no project path left. - last_command = commands_list[-1] if commands_list else "" - print( - f"ERROR: missing after command '{last_command}'.", - file=sys.stderr, - ) - work_usage(program_name) - return 1 - - if not other_root_path.is_dir(): - print(f"ERROR: {other_root_path} is not a directory.", file=sys.stderr) - work_usage(program_name) - return 1 - - # 4. Expand 'all' into its constituent checks - expanded_commands_list: List[str] = [] - if "all" in commands_list and len(commands_list) > 1: - print("ERROR: 'all' cannot be combined with other commands.", file=sys.stderr) - work_usage(program_name) - return 1 - - for command in commands_list: - if command == "all": - expanded_commands_list.extend([ - "environment", - "structure", - "import", - "export", - "suspicious", - "addendum", - ]) - else: - expanded_commands_list.append(command) - - commands_list = expanded_commands_list - - # 5. Execute commands - other_root: Path | None = other_root_path - - for command in commands_list: - print(f"\\n--- Running: {command} ---") - if command == "environment": - work_environment() - elif command == "structure": - if other_root is None: - print("ERROR: 'structure' requires .", file=sys.stderr) - work_usage(program_name) - return 1 - work_structure(other_root) - elif command == "import": - if other_root is None: - print("ERROR: 'import' requires .", file=sys.stderr) - work_usage(program_name) - return 1 - work_import(other_root) - elif command == "export": - if other_root is None: - print("ERROR: 'export' requires .", file=sys.stderr) - work_usage(program_name) - return 1 - work_export(other_root) - elif command == "suspicious": - if other_root is None: - print("ERROR: 'suspicious' requires .", file=sys.stderr) - work_usage(program_name) - return 1 - work_suspicious(other_root) - elif command == "addendum": - if other_root is None: - print("ERROR: 'addendum' requires .", file=sys.stderr) - work_usage(program_name) - return 1 - work_addendum(other_root) - else: - # Should be unreachable because we validated commands_list. - print(f"Unknown command: {command}") - work_usage(program_name) - return 1 - - return 0 - - -if __name__ == "__main__": - sys.exit(CLI()) diff --git a/tool/deprecated/skeleton_commands.py b/tool/deprecated/skeleton_commands.py deleted file mode 100644 index 32520c0..0000000 --- a/tool/deprecated/skeleton_commands.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -""" -skeleton_commands — user-facing command implementations for skeleton_diff -""" - -import os -import sys -from pathlib import Path -from typing import List - -from skeleton_config import HARMONY_ROOT, VERSION -from skeleton_paths import index_project -from skeleton_compare import compare_harmony_to_other - - -# ---------------------------------------------------------------------- -# environment -# ---------------------------------------------------------------------- -def work_environment() -> int: - print("=== Environment ===") - print(f"REPO_HOME = {HARMONY_ROOT}") - for key, value in sorted(os.environ.items()): - if key.startswith(("HARMONY_", "REPO_", "PATH")) or "tool" in key.lower(): - print(f"{key} = {value}") - return 0 - - -# ---------------------------------------------------------------------- -# structure -# ---------------------------------------------------------------------- -def work_structure(other_root: Path) -> int: - print("=== Structure Comparison ===") - - results = compare_harmony_to_other(other_root) - - # Only consider directories for structural reports - skeleton_dir_list = [ - rel for rel, info in results.skeleton_dict.items() - if info.path_type == "dir" - ] - other_dir_list = [ - rel for rel, info in results.other_dict.items() - if info.path_type == "dir" - ] - - skeleton_dir_set = set(skeleton_dir_list) - other_dir_set = set(other_dir_list) - - missing_dir_list = sorted( - skeleton_dir_set - other_dir_set, - key=lambda p: (len(p.parts), str(p)), - ) - - new_dir_list = sorted( - other_dir_set - skeleton_dir_set, - key=lambda p: (len(p.parts), str(p)), - ) - - if missing_dir_list: - print("Missing Harmony directories in :") - for rel_dir in missing_dir_list: - print(f" [MISSING] {rel_dir}") - print() - - if new_dir_list: - print("New directories present only in :") - for rel_dir in new_dir_list: - print(f" [NEW] {rel_dir}") - print() - - if not missing_dir_list and not new_dir_list: - print("No structural directory differences detected.") - - return 0 - - -# ---------------------------------------------------------------------- -# import / export -# ---------------------------------------------------------------------- -def work_import(other_root: Path) -> int: - print("=== Import Commands (newer → Harmony) ===") - - results = compare_harmony_to_other(other_root) - newer_list = results.newer_list - - if not newer_list: - print(" No newer files in to import.") - return 0 - - for rel_path in newer_list: - src = other_root / rel_path - dst = HARMONY_ROOT / rel_path - print(f"cp {src} {dst} # clobbers older Harmony file") - - return 0 - - -def work_export(other_root: Path) -> int: - print("=== Export Commands (Harmony → ) ===") - - results = compare_harmony_to_other(other_root) - older_list = results.older_list - - if not older_list: - print(" No stale files in to export.") - return 0 - - for rel_path in older_list: - src = HARMONY_ROOT / rel_path - dst = other_root / rel_path - print(f"cp {src} {dst} # clobbers stale file in ") - - return 0 - - -# ---------------------------------------------------------------------- -# suspicious -# ---------------------------------------------------------------------- -USER_OWNED_TOP = { - "developer", - "tester", - "release", -} -TOOL_TOP = { - "tool", - "tool_shared", -} - - -def work_suspicious(other_root: Path) -> int: - print("=== Suspicious Files (clutter outside expected zones) ===") - - results = compare_harmony_to_other(other_root) - skeleton_dict = results.skeleton_dict - other_dict = results.other_dict - - suspicious_list: List[Path] = [] - - for rel_path, other_info in other_dict.items(): - if other_info.path_type != "file": - continue - - if rel_path == Path("."): - continue - - top_component = rel_path.parts[0] - - # Skip user-owned zones - if top_component in USER_OWNED_TOP: - continue - - # Skip tool zones - if top_component in TOOL_TOP: - continue - - # If Harmony knows about this file, it is not suspicious. - if rel_path in skeleton_dict: - continue - - suspicious_list.append(rel_path) - - suspicious_list.sort(key=lambda p: (len(p.parts), str(p))) - - if suspicious_list: - for rel_path in suspicious_list: - print(f" [SUSPICIOUS] {rel_path}") - else: - print(" None found.") - - return 0 - - -# ---------------------------------------------------------------------- -# addendum -# ---------------------------------------------------------------------- -def work_addendum(other_root: Path) -> int: - print("=== Addendum: New Tools in ===") - - results = compare_harmony_to_other(other_root) - skeleton_dict = results.skeleton_dict - other_dict = results.other_dict - - addendum_list: List[Path] = [] - - for rel_path, other_info in other_dict.items(): - if other_info.path_type != "file": - continue - - if rel_path == Path("."): - continue - - parts = rel_path.parts - if not parts: - continue - - top_component = parts[0] - if top_component not in TOOL_TOP: - continue - - if rel_path not in skeleton_dict: - addendum_list.append(rel_path) - - addendum_list.sort(key=lambda p: (len(p.parts), str(p))) - - if addendum_list: - for rel_path in addendum_list: - print(f" [ADDENDUM] {rel_path}") - else: - print(" None found.") - - return 0 - - -# ---------------------------------------------------------------------- -# version / usage / help (help text lives in skeleton_diff_docs) -# ---------------------------------------------------------------------- -def work_version() -> int: - print(f"skeleton_diff version {VERSION}") - return 0 - - -def work_usage(program_name: str) -> int: - print(f"Usage: {program_name} []... []") - print() - print(" is required if any commands are specified that") - print("require a project to analyze.") - print() - print("Commands:") - print(" version Show program version (Major.Minor)") - print(" help Long-form documentation") - print(" usage This short summary") - print(" environment Show key environment variables (including $REPO_HOME)") - print(" structure Compare directory structure") - print(" import Print shell commands for pulling newer skeleton") - print(" files into Harmony") - print(" export Print shell commands for pushing Harmony skeleton") - print(" files into ") - print(' suspicious List "between" files that are not in the skeleton') - print(" addendum List tool files in that do not exist in") - print(" the Harmony skeleton (project-local additions)") - print(" all Run the full set of analyses") - print() - print("Examples:") - print(f" {program_name} usage") - print(f" {program_name} structure import ../subu") - print(f" {program_name} all ../subu") - print() - print(f"Run '{program_name} help' for detailed explanations.") - return 0 diff --git a/tool/deprecated/skeleton_compare.py b/tool/deprecated/skeleton_compare.py deleted file mode 100644 index ac76673..0000000 --- a/tool/deprecated/skeleton_compare.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -""" -skeleton_compare — comparison logic between Harmony skeleton and -""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Set - -from skeleton_config import HARMONY_ROOT -from skeleton_paths import NodeInfo, index_project - - -@dataclass -class ComparisonResults: - skeleton_dict: Dict[Path, NodeInfo] - other_dict: Dict[Path, NodeInfo] - missing_list: List[Path] - addendum_list: List[Path] - newer_list: List[Path] - older_list: List[Path] - - -def compare_harmony_to_other(other_root: Path) -> ComparisonResults: - """ - Build Harmony and indexes and compare them. - - - missing_list: paths present in Harmony, absent in . - - addendum_list: paths present in , absent in Harmony. - - newer_list: file paths where is newer than Harmony. - - older_list: file paths where is older than Harmony. - """ - other_root = other_root.resolve() - - skeleton_dict = index_project(HARMONY_ROOT) - other_dict = index_project(other_root) - - skeleton_paths_set: Set[Path] = set(skeleton_dict.keys()) - other_paths_set: Set[Path] = set(other_dict.keys()) - - missing_list: List[Path] = sorted( - skeleton_paths_set - other_paths_set, - key=lambda p: (len(p.parts), str(p)), - ) - - addendum_list: List[Path] = sorted( - other_paths_set - skeleton_paths_set, - key=lambda p: (len(p.parts), str(p)), - ) - - newer_list: List[Path] = [] - older_list: List[Path] = [] - - for rel_path in sorted( - skeleton_paths_set & other_paths_set, - key=lambda p: (len(p.parts), str(p)), - ): - skeleton_info = skeleton_dict[rel_path] - other_info = other_dict[rel_path] - - if skeleton_info.path_type != "file" or other_info.path_type != "file": - continue - - if other_info.mtime > skeleton_info.mtime: - newer_list.append(rel_path) - elif other_info.mtime < skeleton_info.mtime: - older_list.append(rel_path) - - return ComparisonResults( - skeleton_dict=skeleton_dict, - other_dict=other_dict, - missing_list=missing_list, - addendum_list=addendum_list, - newer_list=newer_list, - older_list=older_list, - ) diff --git a/tool/deprecated/skeleton_config.py b/tool/deprecated/skeleton_config.py deleted file mode 100644 index 1724b20..0000000 --- a/tool/deprecated/skeleton_config.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -""" -skeleton_config — shared configuration for skeleton_diff - -Version: Major.Minor = 0.6 -Author: Thomas Walker Lynch, with Grok and Vaelorin -Date: 2025-11-18 -""" - -import os -import sys -from pathlib import Path - -MAJOR = 0 -MINOR = 6 -VERSION = f"{MAJOR}.{MINOR}" - - -def _discover_harmony_root() -> Path: - repo_home = os.getenv("REPO_HOME") - if repo_home: - root_path = Path(repo_home).resolve() - else: - # Fallback: assume current working directory is inside Harmony - root_path = Path.cwd().resolve() - if not root_path.exists(): - print("ERROR: $REPO_HOME not set or invalid. Source env_toolsmith.", file=sys.stderr) - sys.exit(1) - return root_path - - -HARMONY_ROOT = _discover_harmony_root() diff --git a/tool/deprecated/skeleton_diff_docs.py b/tool/deprecated/skeleton_diff_docs.py deleted file mode 100644 index 3db2d1e..0000000 --- a/tool/deprecated/skeleton_diff_docs.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python3 -""" -skeleton_diff_docs — long-form help text for skeleton_diff -""" - -from pathlib import Path -from skeleton_config import VERSION - - -def work_help() -> int: - help_text = f""" -skeleton_diff — Harmony Skeleton Auditor -======================================== - -Version: {VERSION} - -1. Purpose -1.1 The skeleton_diff tool compares a Harmony project (the skeleton) with - another project () that was originally cloned from Harmony. -1.2 Over time, individual projects tend to evolve: - - Some improvements are made in projects but never pulled back to the - Harmony skeleton. - - Some improvements make it back into Harmony, leaving older projects - with stale copies of skeleton files. - - Extra directories and files appear in projects, some intentional and - some accidental. -1.3 skeleton_diff helps you see that drift clearly so that you can: - - Pull newer tooling back into the skeleton. - - Push newer skeleton files out into projects. - - Spot suspicious clutter and structural misuse of the skeleton. - -2. Invocation and Argument Rules -2.1 Basic command line form: - skeleton_diff []... [] -2.2 is required if any of the specified commands - require a project to analyze. -2.3 Commands are parsed from left to right as a list. The final argument - is interpreted as only if: - 2.3.1 At least one command that requires a project appears earlier in - the argument list, and - 2.3.2 There is at least one argument left after that command. -2.4 Dominating commands: - 2.4.1 If any of the following appear anywhere on the command line: - usage, help, version - then that command is executed and all other arguments are - ignored (including other commands and paths). - 2.4.2 This makes: - skeleton_diff usage - skeleton_diff usage . - skeleton_diff version structure ../subu - all behave as simple “usage” or “version” calls. -2.5 Commands that require : - 2.5.1 structure - 2.5.2 import - 2.5.3 export - 2.5.4 suspicious - 2.5.5 addendum - 2.5.6 all (which expands to a sequence of project commands) -2.6 Commands that do not require a project: - 2.6.1 version - 2.6.2 help - 2.6.3 usage - 2.6.4 environment -2.7 Missing project argument: - 2.7.1 If the first command that requires a project is also the last - argument, there is no argument left to serve as - , and skeleton_diff reports an error. - 2.7.2 If a command that requires a project appears before the last - argument, the last argument is interpreted as , even if - its spelling matches a command name. -2.8 Effect of “all”: - 2.8.1 The special command “all” is shorthand for: - environment, structure, import, export, suspicious, addendum - 2.8.2 “all” may not be combined with other commands. If present, it - must be the only non-dominating command on the line. - -3. Environment Expectations -3.1 Before running skeleton_diff you are expected to: - 3.1.1 Be inside a Harmony-derived project. - 3.1.2 Have already run: - source env_toolsmith - which in turn sources: - tool_shared/bespoke/env - 3.1.3 Have $REPO_HOME set to your Harmony project root. -3.2 All paths reported by skeleton_diff are relative to $REPO_HOME unless - otherwise stated. -3.3 The tool does not modify any files. It only reports differences and - prints suggested copy commands for you to run (or edit) manually. - -4. Key Concepts -4.1 Harmony root - 4.1.1 The Harmony skeleton lives at: - $REPO_HOME - 4.1.2 The tool treats $REPO_HOME as the reference layout. -4.2 project root - 4.2.1 The project root is the final argument when a project - is required, and must be a directory. - 4.2.2 It is expected to have been created by cloning the Harmony - skeleton at some point. -4.3 “Between” and “below” paths - 4.3.1 A skeleton directory is any directory that exists in the Harmony - tree, rooted at $REPO_HOME. - 4.3.2 “Between” paths: - - Consider any traversal from the project root down to a known - Harmony skeleton directory (for example, tool/, developer/, - tester/, document/, etc.). - - Any extra directories that appear along those routes in - , but do not exist in the Harmony skeleton, are - “between” paths. - - Example: Harmony has tool/CLI/, but has tool/custom/CLI/. - The inserted custom/ is a “between” directory. - - These often indicate that the developer has modified the - skeleton’s core structural spine. - 4.3.3 “Below” paths: - - Directories that are nested under existing skeleton - directories (for example, tool/custom/new_script.py under - tool/). - - These are usually normal and represent project-specific - organization and additions. - 4.3.4 skeleton_diff reports both: - - “Between” additions, which are more likely to be structural - misuse or deliberate skeleton changes. - - “Below” additions, which are more likely to be healthy - extensions of the skeleton. -4.4 User-owned vs shared zones - 4.4.1 Some areas are explicitly owned by a role and are not audited as - suspicious: - - $REPO_HOME/developer - - $REPO_HOME/tester - - $REPO_HOME/release - 4.4.2 Tools are expected under: - - $REPO_HOME/tool - - $REPO_HOME/tool_shared - 4.4.3 Directories that are covered by a .gitignore are treated as - intentionally messy and are not reported as suspicious clutter. - -5. Commands -5.1 version, help, usage (dominating, no project required) - 5.1.1 version - - Prints: - skeleton_diff version . - - Major and Minor are integers, not a decimal number. - 5.1.2 help - - Shows this detailed documentation. - 5.1.3 usage - - Shows a short, to-the-point command summary and examples. - 5.1.4 If any of these appear, skeleton_diff executes that command and - ignores all other arguments. - -5.2 environment (no project required) - 5.2.1 Prints the key environment context that skeleton_diff relies on: - - $REPO_HOME - - Other Harmony-related variables (e.g., HARMONY_* and REPO_*) - - PATH and selected tool-related variables. - 5.2.2 Useful for debugging configuration or confirming that - env_toolsmith has been sourced correctly. - -5.3 structure (requires ) - 5.3.1 Goal: discover structural drift in directories between Harmony - and . - 5.3.2 Step 1: missing Harmony directories in - - Walk the Harmony skeleton to find all directories under - $REPO_HOME. - - For each Harmony directory, skeleton_diff checks whether the - corresponding directory exists at the same relative path in - . - - Any directory that is present in Harmony but missing in - is reported as: - [MISSING] - 5.3.3 Step 2: new directories in that are not in Harmony - - Walk all directories in . - - Any directory that does not appear in Harmony at the same - relative path is considered “new”. - - New directories are classified as “between” or “below” - according to the rules in section 4.3 and reported so you can - see both structural misuse and legitimate extensions. - -5.4 import / export (both require ) - 5.4.1 Internal files step: - - Both import and export depend on a traversal of skeleton and - project files to classify them as missing/newer/older. - - The traversal is performed automatically as needed; there is no - user-visible “files” command. - 5.4.2 import - - Goal: help you pull improvements *from* *into* Harmony. - - Uses the list of files where is newer than Harmony to - generate shell commands of the form: - cp / $REPO_HOME/ - - Commands overwrite the older skeleton file. The old version is - preserved by git history. - 5.4.3 export - - Goal: help you push newer skeleton files *from* Harmony *into* - . - - Uses the list of files where is older than Harmony to - generate shell commands of the form: - cp $REPO_HOME/ / - - Commands overwrite stale files in . - 5.4.4 Both commands only print shell commands; they never execute them. - -5.5 suspicious (requires ) - 5.5.1 Goal: find “clutter” files that look out of place relative to the - Harmony skeleton. - 5.5.2 A file is not considered suspicious if it is: - - Under $REPO_HOME/developer, which is owned by the developer. - - Under $REPO_HOME/tester, which is owned by the tester. - - Under $REPO_HOME/release, which is deliberately customized by - the developer. - - Under $REPO_HOME/tool or $REPO_HOME/tool_shared, where tools - are expected to live (including substructures). - - In a directory that carries a .gitignore file, which signals - that local clutter is intended and version-control rules exist. - - Present in the Harmony skeleton itself at the same relative - path (i.e., an expected file). - 5.5.3 Any other file that appears in but has no corresponding - skeleton file and is not in the roles’ owned zones or .gitignored - directories is reported as: - [SUSPICIOUS] - 5.5.4 These “between” files are candidates to: - - Move under proper tool directories. - - Add to the skeleton. - - Remove from the project. - -5.6 addendum (requires ) - 5.6.1 Goal: find project-specific tools that might be candidates to - promote back into the Harmony skeleton. - 5.6.2 For each tool directory in : - - tool/ - - tool_shared/ - skeleton_diff walks all files under those directories and - compares them to the Harmony tool directories at the same - relative paths. - 5.6.3 Any file that exists in ’s tool or tool_shared directory - but not in the Harmony skeleton is reported as: - [ADDENDUM] - 5.6.4 These represent project-local tooling; you decide whether to: - - Keep them project-specific, or - - Move them into the shared skeleton. - -5.7 all (requires ) - 5.7.1 all is shorthand for running: - environment - structure - import - export - suspicious - addendum - 5.7.2 It may not be combined with other commands. If you need a - different sequence, list the commands explicitly. - -6. Example Workflows -6.1 Inspect a specific project’s drift - 6.1.1 From a Harmony project: - source env_toolsmith - skeleton_diff all ../subu - 6.1.2 Read: - - environment: sanity-check $REPO_HOME and related variables. - - structure: to see missing or extra directories. - - import/export: to copy improvements across. - - suspicious/addendum: to see clutter and candidate tools. -6.2 Import improvements from a project - 6.2.1 Run: - skeleton_diff import ../subu - 6.2.2 Review the printed cp commands, then run them selectively. -6.3 Refresh a stale project from the skeleton - 6.3.1 Run: - skeleton_diff export ../some_project - 6.3.2 Review cp commands, run them, and then commit in . -6.4 Quick environment and doc checks - 6.4.1 Without a project: - skeleton_diff usage - skeleton_diff help - skeleton_diff version - skeleton_diff environment - -7. Safety and Limitations -7.1 No automatic writes - 7.1.1 skeleton_diff never changes files itself. It only prints commands - and reports. -7.2 Time-based comparison - 7.2.1 “Newer” and “older” are based on filesystem modification times. - If clocks or timestamps are misleading, results may need manual - interpretation. -7.3 Directory semantics - 7.3.1 “Between” and “below” classification is a heuristic based on - the current Harmony skeleton. Some edge cases may require human - judgment. -7.4 Git integration - 7.4.1 The tool assumes that Harmony is a git repository and relies on - git history for old versions. It does not attempt to archive or - back up overwritten files. -7.5 Exit Status - 7.5.1 skeleton_diff returns: - - 0 on success (even if differences are found). - - Non-zero if arguments are invalid (e.g., missing project path) - or if a subcommand fails before producing output. -""" - print(help_text.strip()) - return 0 diff --git a/tool/deprecated/skeleton_paths.py b/tool/deprecated/skeleton_paths.py deleted file mode 100644 index 521d728..0000000 --- a/tool/deprecated/skeleton_paths.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -""" -skeleton_paths — filesystem indexing and ignore logic for skeleton_diff -""" - -import os -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, Set, List - - -# ---------------------------------------------------------------------- -# Node information -# ---------------------------------------------------------------------- - -@dataclass -class NodeInfo: - path_type: str # "file", "dir", or "other" - mtime: float - is_leaf_flag: bool = False - - -# ---------------------------------------------------------------------- -# Built-in ignore rules and helper -# ---------------------------------------------------------------------- - -def _is_builtin_ignored(rel_path: Path) -> bool: - """ - Cheap ignore filter for common junk: - - .git tree - - __pycache__ and Python bytecode - - notebook / pytest caches - - editor backup files (*~, *.bak) - """ - parts = rel_path.parts - - # Ignore anything under a .git directory - if ".git" in parts: - return True - - name = parts[-1] if parts else "" - - # Directories by name - if name in { - "__pycache__", - ".ipynb_checkpoints", - ".pytest_cache", - }: - return True - - # Python bytecode / compiled - lower_name = name.lower() - if lower_name.endswith(".pyc") or lower_name.endswith(".pyo") or lower_name.endswith(".pyd"): - return True - - # Editor backup / temp - if lower_name.endswith("~") or lower_name.endswith(".bak"): - return True - - return False - - -def should_ignore_node(project_root: Path, rel_path: Path) -> bool: - """ - Single "ignore this?" decision point. - - For now we: - - apply built-in patterns, and - - treat any directory that has a .gitignore file as fully ignored - (except for the .gitignore file itself). - - TODO: - - Parse .gitignore files properly and obey their patterns. - """ - if not rel_path.parts: - # Root is never ignored. - return False - - # Built-in patterns - if _is_builtin_ignored(rel_path): - return True - - absolute_path = project_root / rel_path - parent_dir = absolute_path.parent - - # If parent directory has a .gitignore, ignore this node. - # (We do not ignore the .gitignore file itself.) - if (parent_dir / ".gitignore").exists() and absolute_path.name != ".gitignore": - return True - - return False - - -# ---------------------------------------------------------------------- -# Indexing -# ---------------------------------------------------------------------- - -def _make_node_info(path: Path) -> NodeInfo: - if path.is_file(): - path_type = "file" - elif path.is_dir(): - path_type = "dir" - else: - path_type = "other" - - try: - mtime = path.stat().st_mtime - except OSError: - mtime = 0.0 - - return NodeInfo( - path_type=path_type, - mtime=mtime, - is_leaf_flag=False, - ) - - -def index_project(project_root: Path) -> Dict[Path, NodeInfo]: - """ - Walk an entire project tree and build a dictionary mapping - relative paths -> NodeInfo, applying the ignore filter. - - The relative path Path(".") is used for the project root. - """ - project_root = project_root.resolve() - node_dict: Dict[Path, NodeInfo] = {} - - # Always register the root - root_rel_path = Path(".") - node_dict[root_rel_path] = _make_node_info(project_root) - - for dirpath_str, dirnames_list, filenames_list in os.walk(project_root): - dirpath = Path(dirpath_str) - rel_dir = dirpath.relative_to(project_root) - - # Filter directory traversal in-place so os.walk will skip ignored dirs. - keep_dirnames_list: List[str] = [] - for dirname in dirnames_list: - child_rel = (rel_dir / dirname) if rel_dir != Path(".") else Path(dirname) - if should_ignore_node(project_root, child_rel): - continue - keep_dirnames_list.append(dirname) - dirnames_list[:] = keep_dirnames_list - - # Ensure directory node itself is recorded - if rel_dir not in node_dict: - node_dict[rel_dir] = _make_node_info(dirpath) - - # Record files - for filename in filenames_list: - rel_file = (rel_dir / filename) if rel_dir != Path(".") else Path(filename) - if should_ignore_node(project_root, rel_file): - continue - abs_file = dirpath / filename - node_dict[rel_file] = _make_node_info(abs_file) - - # Second pass: mark leaf directories - parent_dir_set: Set[Path] = set() - for rel_path in node_dict.keys(): - if rel_path == Path("."): - continue - parent = rel_path.parent - parent_dir_set.add(parent) - - for rel_path, info in node_dict.items(): - if info.path_type == "dir": - if rel_path not in parent_dir_set: - info.is_leaf_flag = True - - return node_dict diff --git a/tool/skel.tgz b/tool/skel.tgz deleted file mode 100644 index 7e2bf1ac079b9e66b8b5ddb9a7fe4e38d7e431f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35579 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

F~#fW=2 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 WF~>0N|0mO-6mDUtMJ1IY4b^{Yr09tN -- 2.20.1