Skip to content

Scripts Reference

Language-agnostic: the scanning workflow applies to any project. The scripts support Python (via its ast module) and PHP (via tree-sitter-php). For other languages, apply the principles manually by reading the code — the scripts are a triage accelerator, not a requirement.

All scripts live in per-skill subfolders under src/zolletta_metaskill/ (patterns/, patterns/php/, code_style/general/, code_style/python/, testing_style/general/, testing_style/python/). Language-agnostic scanners consume a ModuleInfo data model produced by a LanguageEngine — no code execution required.

Every script supports --skip (exit 0 with "SKIPPED" message) for projects that intentionally don't follow a given convention. Scripts that report violations also support --strict (exit code 1 if violations found).

Triage Scripts

class_metrics_scanner.py

Scans all .py files and reports every class sorted by line count, with method count, public method count, and self.* attribute count.

python3 src/zolletta_metaskill/patterns/class_metrics_scanner.py <directory> [--top N] [--min-lines N]
Option Default Description
<directory> src Root directory to scan
--top N 30 Show only the top N classes
--min-lines N 50 Skip classes shorter than N lines

Output: a table with columns LINES, ALL (methods), PUB (public methods), ATTRS (self.* attributes), CLASS, and file:start-end.

Use the output to identify candidates, then read the code to apply the "reason to change" test.

test_god_classes_scanner.py

Scans test files and reports test classes sorted by size, with method count and method names. Detects test classes that test multiple unrelated SUTs.

python3 src/zolletta_metaskill/patterns/test_god_classes_scanner.py <directory> [--top N] [--show-methods]
Option Default Description
<directory> tests Root directory to scan
--top N 30 Show only the top N classes
--show-methods off List all method names per class (helps spot mixed SUTs)

Structural Convention Scripts

one_class_per_file_scanner.py

Checks the "1 class 1 file, 1 file 1 class" convention. Reports files with 2+ classes, files with 0 classes (non-__init__.py), and class names that don't match the filename.

python3 src/zolletta_metaskill/code_style/general/one_class_per_file_scanner.py <directory> [--strict] [--ignore-zero] [--skip]
Option Default Description
<directory> src Root directory to scan
--strict off Exit with code 1 if violations are found
--ignore-zero off Don't report files with 0 classes (utility modules)
--skip off Skip this check entirely

Exceptions: __init__.py is always skipped. Files with 0 classes are reported as low severity — use --ignore-zero to hide them.

test_structure_scanner.py

Checks that the test directory structure mirrors the source directory structure. Outputs a markdown report with five tables:

  1. Misnamed tests — test files whose name doesn't match the source stem or class name of the source they test. Action: rename.
  2. Misplaced tests — test files with a name matching a source file but located in the wrong directory. Action: move.
  3. Orphaned tests — test files or directories that don't match any source file or directory. Action: delete or investigate.
  4. Missing tests — source files with classes that have no direct test file and no indirect class reference in any test file. Action: write new tests.
  5. Indirect references — test files that reference classes from source files without a direct test. Informative only: shows which test files provide indirect coverage for otherwise untested source files.
python3 src/zolletta_metaskill/testing_style/general/test_structure_scanner.py \
    --src <src_root> --tests <test_root> \
    [--src-package <name>] [--tests-package <name>] \
    [--ignore-dirs <dir1,dir2,...>] [--skip]
Option Default Description
--src src Source root directory
--tests tests Test root directory
--src-package auto-detect Package path within --src
--tests-package same as --src-package Package path within --tests
--ignore-dirs (none) Comma-separated dir names to skip (e.g. assets,templates)
--skip off Skip this check entirely

File matching convention: Test files match source files by stem prefix: test_cache_* matches cache.py.

naming_conventions_scanner.py

Checks two naming conventions in a single pass:

  1. Source file name == class name — each source file with exactly one class should have a filename matching the class name (snake_case file → PascalCase class). Files with 0 or 2+ classes are skipped (handled by one_class_per_file_scanner.py).
  2. Test file naming — every test_*.py file must follow test_<source_stem><eventual_suffix>.py, where <source_stem> is the stem of a source file (or the snake_case form of a source class name) in the mirrored source directory. Test files that don't match any source file or class are reported as orphan/misnamed.
python3 src/zolletta_metaskill/code_style/general/naming_conventions_scanner.py \
    --src <src_root> --tests <test_root> \
    [--src-package <name>] [--tests-package <name>] \
    [--ignore-dirs <dir1,dir2,...>] [--strict] [--skip]
Option Default Description
--src src Source root directory
--tests tests Test root directory
--src-package auto-detect Package path within --src
--tests-package same as --src-package Package path within --tests
--ignore-dirs (none) Comma-separated dir names to skip (e.g. assets,templates)
--strict off Exit with code 1 if violations are found
--skip off Skip this check entirely

Matching logic: Longest stem-prefix match wins; class-name prefixes also checked.

SOLID Validator Scripts

dependency_inversion_scanner.py (DIP, Python)

Detects classes that instantiate their dependencies internally (self.x = SomeClass(...)) instead of receiving them as constructor parameters. Excludes entry points, dataclasses, factories, and stdlib types.

python3 src/zolletta_metaskill/patterns/dependency_inversion_scanner.py <directory>
    [--entry-points <pattern1,pattern2,...>] [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--entry-points main,cli,app,__main__,myproject,manage,wsgi,asgi,conftest Comma-separated filename patterns to exclude
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

Exclusions: Excludes entry points, DI containers, dataclasses, factories, and stdlib types.

interface_segregation_scanner.py (ISP, Python)

Detects fat interfaces — Protocols/ABCs with many methods where implementers stub or raise NotImplementedError for methods they don't need.

python3 src/zolletta_metaskill/patterns/interface_segregation_scanner.py <directory> [--min-methods N] [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--min-methods N 5 Minimum abstract method count to flag as fat
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

Checks: Protocol/ABC classes with N+ methods, implementers that raise NotImplementedError or have stub bodies (pass/return None) for interface methods.

open_closed_scanner.py (OCP, Python)

Detects type-based branching (if/elif isinstance ladders, match/case on type, getattr string dispatch) that should be replaced with polymorphism.

python3 src/zolletta_metaskill/patterns/open_closed_scanner.py <directory> [--min-branches N] [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--min-branches N 3 Minimum type-check branches to flag
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

liskov_substitution_scanner.py (LSP)

Detects subclass methods that break substitutability: incompatible signatures, new exception types, empty-body overrides.

python3 src/zolletta_metaskill/patterns/liskov_substitution_scanner.py <directory> [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

Checks: overridden methods with extra required params, fewer params than parent, new exception types, stub overrides (pass/return None when parent has a real body).

PHP SOLID Validator Scripts

These scanners live in src/zolletta_metaskill/patterns/php/ and target PHP codebases. They use the PHPEngine (tree-sitter-php) to parse .php files. Install the optional dependency with uv pip install zolletta-metaskill[php].

dependency_inversion_scanner.py (DIP, PHP)

Detects classes that instantiate their dependencies internally (new ConcreteClass() in constructors or methods) instead of receiving them via dependency injection. Excludes factories, builders, and PHP built-in types.

python3 src/zolletta_metaskill/patterns/php/dependency_inversion_scanner.py <directory> [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

How it works: since ModuleInfo does not capture new expressions, this scanner calls PHPEngine.parse_raw() to access the tree-sitter AST directly and walks it for new_expression nodes inside class methods.

Exclusions: classes whose name contains Factory or Builder are treated as composition roots where object creation is expected. PHP built-in types (stdClass, DateTime, Exception, etc.) are excluded from dependency detection.

interface_segregation_scanner.py (ISP, PHP)

Detects fat interfaces — PHP interfaces with many methods where implementers are forced to depend on methods they do not use.

python3 src/zolletta_metaskill/patterns/php/interface_segregation_scanner.py <directory> [--min-methods N] [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--min-methods N 7 Minimum method count to flag as fat
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

How it works: uses ModuleInfo directly (no raw AST needed). PHP interfaces are mapped to ClassInfo with is_abstract=True and no attributes. Interfaces with more than --min-methods methods are flagged as fat.

open_closed_scanner.py (OCP, PHP)

Detects if/elseif chains that use instanceof to branch on subtypes — an OCP violation. Adding a new subtype requires modifying the ladder instead of simply adding a new implementation.

python3 src/zolletta_metaskill/patterns/php/open_closed_scanner.py <directory> [--min-branches N] [--skip] [--strict]
Option Default Description
<directory> src Root directory to scan
--min-branches N 3 Minimum instanceof branches to flag
--skip off Skip this check entirely
--strict off Exit with code 1 if violations are found

How it works: since ModuleInfo does not capture instanceof expressions, this scanner calls PHPEngine.parse_raw() to access the tree-sitter AST directly and counts instanceof branches in if_statement nodes.

LanguageEngine Protocol

The LanguageEngine protocol is the seam between language-agnostic scanners and language-specific parsers. Scanners depend only on this protocol and the ModuleInfo data model — they never import ast or tree-sitter directly.

core/language_engine.py

Defines the LanguageEngine protocol (@runtime_checkable). Every engine must implement:

Method / property Description
language Language identifier (e.g. "python", "php")
parse_module(path) Parse a source file and return a ModuleInfo
is_test_file(path) Return True if the path is a test file for this language
is_source_file(path) Return True if the path is a source file for this language
file_extensions() Return the list of extensions handled (e.g. [".py"], [".php"])
test_file_glob() Return the glob pattern for test files (e.g. "test_*.py", "*Test.php")

core/registry.py

Provides the engine registry — maps language names and file extensions to engines:

Function Description
register(engine) Register an engine under its language identifier (raises ValueError on duplicate)
get(language) Return the registered engine for a language (raises KeyError if not found)
get_for_file(path) Return the engine that handles a file path based on its extension, or None
available_languages() Return a sorted list of registered language identifiers

engines/python_engine.py & engines/php_engine.py

Two implementations of the LanguageEngine protocol:

  • PythonEngine — wraps Python's ast module to parse .py files into ModuleInfo.
  • PHPEngine — wraps tree-sitter with the tree-sitter-php grammar to parse .php files into ModuleInfo. The tree-sitter-php package is an optional dependency (uv pip install zolletta-metaskill[php]); if not installed, the engine still instantiates but parse_module() raises a clear ImportError.

PHPEngine.parse_raw()

def parse_raw(self, path: Path) -> tuple[Tree, bytes]

Returns the raw tree-sitter Tree and source bytes for a .php file. Used by PHP-specific scanners (dependency_inversion_scanner.py, open_closed_scanner.py) that need direct AST access for constructs not captured in ModuleInfo (e.g. new expressions, instanceof chains). The source bytes are needed to extract text from individual nodes via source[node.start_byte:node.end_byte].

Parameters:

  • path — Path to the .php file to parse.

Dead Code Script

unused_all_exports_scanner.py

Finds names listed in __all__ that are never imported by any other module in the source tree. Complements vulture, which treats __all__ entries as "used" (public API exports) and therefore never flags them as dead code — even when no module ever imports them.

python3 src/zolletta_metaskill/code_style/python/unused_all_exports_scanner.py <directory> [--strict] [--json] [--skip]
Option Default Description
<directory> src Root source directory to scan
--strict off Exit with code 1 if unused exports are found
--json off Output as JSON instead of markdown
--skip off Skip this check entirely

test_naming_scanner.py

Checks test function names against the test_<unit>_<scenario>_<expected> convention. Flags functions with fewer than --min-segments underscore-separated segments after the test_ prefix. This is a deterministic replacement for manual review of test function names.

python3 src/zolletta_metaskill/testing_style/python/test_naming_scanner.py <directory> [--min-segments N] [--strict] [--json] [--skip]
Option Default Description
<directory> tests Root test directory to scan
--min-segments N 3 Minimum segments after test_ prefix
--strict off Exit with code 1 if violations are found
--json off Output as JSON instead of markdown
--skip off Skip this check entirely

acronym_casing_scanner.py

Checks that acronyms in PascalCase class names stay fully uppercase (e.g. HTTPClientFactory, not HttpClientFactory). The scanner splits each PascalCase class name into words, checks each word against the configured acronym list, and flags any word that case-insensitively matches an acronym but isn't all-uppercase.

python3 src/zolletta_metaskill/code_style/python/acronym_casing_scanner.py <directory> [--acronyms <list>] [--strict] [--json] [--skip]
Option Default Description
<directory> src Root source directory to scan
--acronyms (from assets + settings) Comma-separated acronym list (overrides built-in + settings)
--strict off Exit with code 1 if violations are found
--json off Output as JSON instead of markdown
--skip off Skip this check entirely

The acronym list is built additively:

  1. Shipped base: python-code-style/assets/acronyms.json (common SE acronyms: CI, CD, CICD, HTTP, HTTPS, JSON, SQL, URL, etc.) — always loaded
  2. Project-specific: the top-level acronyms array in settings.json — merged with the shipped list (additive, not replacing)
  3. --acronyms CLI flag: fully replaces both (for testing/debugging only)

test_splitter.py

Automates splitting a test God class that tests multiple SUTs into per-SUT test files.

python3 src/zolletta_metaskill/patterns/test_splitter.py <test_file> [--dry-run] [--output-dir <dir>]
Option Default Description
<test_file> (req) Path to the test file to split
--dry-run off Show what would be split without writing files
--output-dir same dir Directory to write split files to

What the splitter handles automatically:

  • Copies all imports to each split file
  • Copies pytestmark to each split file
  • Copies shared methods (fixtures, helpers) to each split file
  • Generates proper class names (Test<SutName>)
  • Indents methods correctly inside the class
  • Reports unmatched methods for mapping review

What the splitter does NOT do:

  • Remove unused imports from split files (review manually)
  • Move the files to the final test directory (human reviews first)
  • Delete the original file (human confirms the split is correct)
  • Run the tests (human verifies the split files pass)

Complete Workflow

Run scripts in order: triage (class_metrics_scanner, test_god_classes_scanner) → structural (one_class_per_file_scanner, test_structure_scanner, naming_conventions_scanner) → SOLID (dependency_inversion_scanner, interface_segregation_scanner, open_closed_scanner, liskov_substitution_scanner) → PHP SOLID (PHP projects) → dead code (unused_all_exports_scanner) → naming (test_naming_scanner, acronym_casing_scanner) → split (test_splitter). Apply the "reason to change" test to top candidates.

Repository Scripts

These are project-management scripts at the repository root, separate from the scanning scripts above.

install.sh

Installs the skill into ~/.agents/skills/ and symlinks it into every detected AI agent tool's skills directory.

./install.sh           # install/refresh
./install.sh --force   # replace real dirs with symlinks

See Install Zolletta-MetaSkill for details.