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

1#!/usr/bin/env python3 

2"""Does the repo have the shape its language expects? — `validate.py`'s structure half. 

3 

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. 

10 

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""" 

15 

16from __future__ import annotations 

17 

18import sys 

19from collections.abc import Callable 

20from pathlib import Path 

21 

22sys.path.insert(0, str(Path(__file__).resolve().parent)) 

23from _validate_log import Log # noqa: E402 

24 

25 

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'}") 

39 

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 

48 

49 

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'}") 

60 

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}") 

67 

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 

79 

80 

81def validate_rust_structure(log: Log, target: Path) -> bool: 

82 """Rust needs Cargo.toml (required); a src/ crate root is a warning. 

83 

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 

99 

100 log.success(f"Cargo.toml exists: {manifest}") 

101 is_workspace_root = "[workspace]" in manifest.read_text(encoding="utf-8") 

102 

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") 

113 

114 return True 

115 

116 

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} 

124 

125 

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)