Scripts Reference¶
Language-agnostic: the scanning workflow applies to any project. The scripts support Python (via its
astmodule) 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:
- Misnamed tests — test files whose name doesn't match the source stem or class name of the source they test. Action: rename.
- Misplaced tests — test files with a name matching a source file but located in the wrong directory. Action: move.
- Orphaned tests — test files or directories that don't match any source file or directory. Action: delete or investigate.
- 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.
- 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:
- 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). - Test file naming — every
test_*.pyfile must followtest_<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'sastmodule to parse.pyfiles intoModuleInfo.PHPEngine— wraps tree-sitter with the tree-sitter-php grammar to parse.phpfiles intoModuleInfo. Thetree-sitter-phppackage is an optional dependency (uv pip install zolletta-metaskill[php]); if not installed, the engine still instantiates butparse_module()raises a clearImportError.
PHPEngine.parse_raw()¶
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.phpfile 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:
- Shipped base:
python-code-style/assets/acronyms.json(common SE acronyms: CI, CD, CICD, HTTP, HTTPS, JSON, SQL, URL, etc.) — always loaded - Project-specific: the top-level
acronymsarray insettings.json— merged with the shipped list (additive, not replacing) --acronymsCLI 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
pytestmarkto 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.
See Install Zolletta-MetaSkill for details.