Coverage for plugin/scripts/_validate_structure.py: 100%
71 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-15 14:46 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-15 14:46 +0000
1#!/usr/bin/env python3
2"""Does the repo have the shape its language expects? — `validate.py`'s structure half.
4One rule decides everything here: **the manifest is an error, the layout is a warning.**
5A Python repo without `pyproject.toml` cannot be synced at all, so that fails; a Python
6repo without `tests/` is merely unusual, so that warns. Each language draws the same line
7in its own vocabulary, which is why they are three functions and not one parameterised
8one — Go's "`pkg` or `internal`, either will do" has no analogue in Python, and a Rust
9workspace root legitimately has no crate root whatsoever.
11:data:`VALIDATORS` is the registry, and it is also what `_validate_fields` consults to
12decide whether a declared `language:` is one this plugin knows. Adding a language means
13adding one entry here.
14"""
16from __future__ import annotations
18import sys
19from collections.abc import Callable
20from pathlib import Path
22sys.path.insert(0, str(Path(__file__).resolve().parent))
23from _validate_log import Log # noqa: E402
26def validate_python_structure(log: Log, target: Path) -> bool:
27 """Python needs pyproject.toml (required); src/ and tests/ are warnings."""
28 passed = True
29 if not (target / "pyproject.toml").exists():
30 log.error(f"pyproject.toml not found: {target / 'pyproject.toml'}")
31 log.error("pyproject.toml is required for Python projects")
32 log.info(
33 "Run /rhiza:init — or init_skeleton.py directly — to set the repo up, "
34 "which creates a pyproject.toml"
35 )
36 passed = False
37 else:
38 log.success(f"pyproject.toml exists: {target / 'pyproject.toml'}")
40 for name in ("src", "tests"):
41 d = target / name
42 if not d.exists():
43 log.warning(f"Standard '{name}' folder not found: {d}")
44 log.warning(f"Consider creating a '{name}' directory")
45 else:
46 log.success(f"'{name}' folder exists: {d}")
47 return passed
50def validate_go_structure(log: Log, target: Path) -> bool:
51 """Go needs go.mod (required); cmd/ and pkg/|internal/ are warnings."""
52 passed = True
53 if not (target / "go.mod").exists():
54 log.error(f"go.mod not found: {target / 'go.mod'}")
55 log.error("go.mod is required for Go projects")
56 log.info("Run 'go mod init <module-name>' to create go.mod")
57 passed = False
58 else:
59 log.success(f"go.mod exists: {target / 'go.mod'}")
61 cmd_dir, pkg_dir, internal_dir = target / "cmd", target / "pkg", target / "internal"
62 if not cmd_dir.exists():
63 log.warning(f"Standard 'cmd' folder not found: {cmd_dir}")
64 log.warning("Consider creating a 'cmd' directory for main applications")
65 else:
66 log.success(f"'cmd' folder exists: {cmd_dir}")
68 if not pkg_dir.exists() and not internal_dir.exists():
69 log.warning("Neither 'pkg' nor 'internal' folder found")
70 log.warning(
71 "Consider creating 'pkg' for public libraries or 'internal' for private packages"
72 )
73 else:
74 if pkg_dir.exists():
75 log.success(f"'pkg' folder exists: {pkg_dir}")
76 if internal_dir.exists():
77 log.success(f"'internal' folder exists: {internal_dir}")
78 return passed
81def validate_rust_structure(log: Log, target: Path) -> bool:
82 """Rust needs Cargo.toml (required); a src/ crate root is a warning.
84 Cargo puts both library and binary crates under ``src/`` — ``src/lib.rs`` for a
85 library, ``src/main.rs`` for a binary, and a workspace root may legitimately have
86 neither. So the crate root is checked as a warning, not an error, and a virtual
87 workspace (``[workspace]`` with no ``[package]``) is recognised rather than
88 reported as a malformed crate.
89 """
90 manifest = target / "Cargo.toml"
91 if not manifest.exists():
92 log.error(f"Cargo.toml not found: {manifest}")
93 log.error("Cargo.toml is required for Rust projects")
94 log.info(
95 "Run /rhiza:init — or init_skeleton.py directly — to set the repo up, "
96 "which creates a Cargo.toml"
97 )
98 return False
100 log.success(f"Cargo.toml exists: {manifest}")
101 is_workspace_root = "[workspace]" in manifest.read_text(encoding="utf-8")
103 crate_roots = [target / "src" / name for name in ("lib.rs", "main.rs")]
104 found = [p for p in crate_roots if p.exists()]
105 if found:
106 for path in found:
107 log.success(f"crate root exists: {path}")
108 elif is_workspace_root:
109 log.success("no src/ crate root, but Cargo.toml declares a [workspace] — fine")
110 else:
111 log.warning(f"Neither 'src/lib.rs' nor 'src/main.rs' found under {target / 'src'}")
112 log.warning("Consider creating 'src/lib.rs' for a library or 'src/main.rs' for a binary")
114 return True
117# Registry of language -> structure validator; extend here to add a language. Also the
118# authority `_validate_fields` uses for "is this a language we know?".
119VALIDATORS: dict[str, Callable[[Log, Path], bool]] = {
120 "python": validate_python_structure,
121 "go": validate_go_structure,
122 "rust": validate_rust_structure,
123}
126def check_project_structure(log: Log, target: Path, language: str) -> bool:
127 """Dispatch to the language validator; unsupported languages pass with a warning."""
128 log.debug(f"Validating project structure for language: {language}")
129 validator = VALIDATORS.get(language.lower())
130 if validator is None:
131 log.warning(f"No validator found for language '{language}'")
132 log.warning(f"Supported languages: {', '.join(VALIDATORS)}")
133 log.warning("Skipping project structure validation")
134 return True
135 return validator(log, target)