+++ /dev/null
-#!/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 <other_project_path>.
- 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 "<unknown>"
- print(
- f"ERROR: missing <other_project_path> 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 <other_project_path>.", 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 <other_project_path>.", 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 <other_project_path>.", 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 <other_project_path>.", 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 <other_project_path>.", 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())
+++ /dev/null
-#!/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 <other>:")
- for rel_dir in missing_dir_list:
- print(f" [MISSING] {rel_dir}")
- print()
-
- if new_dir_list:
- print("New directories present only in <other>:")
- 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 <other> 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 → <other>) ===")
-
- results = compare_harmony_to_other(other_root)
- older_list = results.older_list
-
- if not older_list:
- print(" No stale files in <other> 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 <other>")
-
- 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 <other> ===")
-
- 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} [<command>]... [<other_project_path>]")
- print()
- print("<other_project_path> 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 <other>")
- print(' suspicious List "between" files that are not in the skeleton')
- print(" addendum List tool files in <other> 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
+++ /dev/null
-#!/usr/bin/env python3
-"""
-skeleton_compare — comparison logic between Harmony skeleton and <other>
-"""
-
-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 <other> indexes and compare them.
-
- - missing_list: paths present in Harmony, absent in <other>.
- - addendum_list: paths present in <other>, absent in Harmony.
- - newer_list: file paths where <other> is newer than Harmony.
- - older_list: file paths where <other> 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,
- )
+++ /dev/null
-#!/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()
+++ /dev/null
-#!/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 (<other>) 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 [<command>]... [<other_project_path>]
-2.2 <other_project_path> 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 <other_project_path> 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 <other_project_path>:
- 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
- <other_project_path>, 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 <other>, 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 <other> project root
- 4.2.1 The <other> 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
- <other>, but do not exist in the Harmony skeleton, are
- “between” paths.
- - Example: Harmony has tool/CLI/, but <other> 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>.<Minor>
- - 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 <other>)
- 5.3.1 Goal: discover structural drift in directories between Harmony
- and <other>.
- 5.3.2 Step 1: missing Harmony directories in <other>
- - 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
- <other>.
- - Any directory that is present in Harmony but missing in <other>
- is reported as:
- [MISSING] <relative/path>
- 5.3.3 Step 2: new directories in <other> that are not in Harmony
- - Walk all directories in <other>.
- - 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 <other>)
- 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* <other> *into* Harmony.
- - Uses the list of files where <other> is newer than Harmony to
- generate shell commands of the form:
- cp <other>/<relative/path> $REPO_HOME/<relative/path>
- - 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*
- <other>.
- - Uses the list of files where <other> is older than Harmony to
- generate shell commands of the form:
- cp $REPO_HOME/<relative/path> <other>/<relative/path>
- - Commands overwrite stale files in <other>.
- 5.4.4 Both commands only print shell commands; they never execute them.
-
-5.5 suspicious (requires <other>)
- 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 <other> but has no corresponding
- skeleton file and is not in the roles’ owned zones or .gitignored
- directories is reported as:
- [SUSPICIOUS] <relative/path>
- 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 <other>)
- 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 <other>:
- - 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 <other>’s tool or tool_shared directory
- but not in the Harmony skeleton is reported as:
- [ADDENDUM] <relative/path>
- 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 <other>)
- 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 <other>.
-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
+++ /dev/null
-#!/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