pyossmtool 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. pyossmtool/__init__.py +3 -0
  2. pyossmtool/bundle/catalog/checks/bandit.scan.yaml +13 -0
  3. pyossmtool/bundle/catalog/checks/codespell.spelling.yaml +12 -0
  4. pyossmtool/bundle/catalog/checks/deadcode.scan.yaml +13 -0
  5. pyossmtool/bundle/catalog/checks/gate.acronym-allowlist.yaml +14 -0
  6. pyossmtool/bundle/catalog/checks/gate.folder-breadth.yaml +14 -0
  7. pyossmtool/bundle/catalog/checks/gate.module-private-vars.yaml +14 -0
  8. pyossmtool/bundle/catalog/checks/gate.module-size.yaml +14 -0
  9. pyossmtool/bundle/catalog/checks/gitleaks.secrets.yaml +21 -0
  10. pyossmtool/bundle/catalog/checks/hadolint.dockerfile.yaml +16 -0
  11. pyossmtool/bundle/catalog/checks/jscpd.duplication.yaml +19 -0
  12. pyossmtool/bundle/catalog/checks/markdownlint.check.yaml +17 -0
  13. pyossmtool/bundle/catalog/checks/mdformat.apply.yaml +10 -0
  14. pyossmtool/bundle/catalog/checks/mdformat.format.yaml +14 -0
  15. pyossmtool/bundle/catalog/checks/mutmut.run.yaml +13 -0
  16. pyossmtool/bundle/catalog/checks/pydeps.cycles.yaml +14 -0
  17. pyossmtool/bundle/catalog/checks/pytest.coverage.yaml +19 -0
  18. pyossmtool/bundle/catalog/checks/pytest.test.yaml +14 -0
  19. pyossmtool/bundle/catalog/checks/radon.cc.yaml +13 -0
  20. pyossmtool/bundle/catalog/checks/radon.mi.yaml +13 -0
  21. pyossmtool/bundle/catalog/checks/ruff.format.apply.yaml +11 -0
  22. pyossmtool/bundle/catalog/checks/ruff.format.yaml +14 -0
  23. pyossmtool/bundle/catalog/checks/ruff.lint.yaml +15 -0
  24. pyossmtool/bundle/catalog/checks/ruff.unused.yaml +17 -0
  25. pyossmtool/bundle/catalog/checks/semgrep.scan.yaml +18 -0
  26. pyossmtool/bundle/catalog/checks/shellcheck.yaml +13 -0
  27. pyossmtool/bundle/catalog/checks/shfmt.apply.yaml +11 -0
  28. pyossmtool/bundle/catalog/checks/shfmt.format.yaml +14 -0
  29. pyossmtool/bundle/catalog/checks/sourcery.review.yaml +14 -0
  30. pyossmtool/bundle/catalog/checks/ty.check.yaml +15 -0
  31. pyossmtool/bundle/catalog/checks/vulture.scan.yaml +16 -0
  32. pyossmtool/bundle/catalog/checks/yamlfmt.apply.yaml +10 -0
  33. pyossmtool/bundle/catalog/checks/yamlfmt.format.yaml +14 -0
  34. pyossmtool/bundle/catalog/checks/yamllint.check.yaml +16 -0
  35. pyossmtool/bundle/catalog/tools/bandit.yaml +20 -0
  36. pyossmtool/bundle/catalog/tools/codespell.yaml +17 -0
  37. pyossmtool/bundle/catalog/tools/deadcode.yaml +13 -0
  38. pyossmtool/bundle/catalog/tools/gitleaks.yaml +9 -0
  39. pyossmtool/bundle/catalog/tools/hadolint.yaml +11 -0
  40. pyossmtool/bundle/catalog/tools/jscpd.yaml +15 -0
  41. pyossmtool/bundle/catalog/tools/markdownlint.yaml +18 -0
  42. pyossmtool/bundle/catalog/tools/mdformat.yaml +13 -0
  43. pyossmtool/bundle/catalog/tools/mutmut.yaml +10 -0
  44. pyossmtool/bundle/catalog/tools/pydeps.yaml +10 -0
  45. pyossmtool/bundle/catalog/tools/pytest.yaml +12 -0
  46. pyossmtool/bundle/catalog/tools/radon.yaml +10 -0
  47. pyossmtool/bundle/catalog/tools/ruff.yaml +26 -0
  48. pyossmtool/bundle/catalog/tools/script.yaml +8 -0
  49. pyossmtool/bundle/catalog/tools/semgrep.yaml +21 -0
  50. pyossmtool/bundle/catalog/tools/shellcheck.yaml +10 -0
  51. pyossmtool/bundle/catalog/tools/shfmt.yaml +13 -0
  52. pyossmtool/bundle/catalog/tools/sourcery.yaml +10 -0
  53. pyossmtool/bundle/catalog/tools/ty.yaml +20 -0
  54. pyossmtool/bundle/catalog/tools/vulture.yaml +13 -0
  55. pyossmtool/bundle/catalog/tools/yamlfmt.yaml +24 -0
  56. pyossmtool/bundle/catalog/tools/yamllint.yaml +24 -0
  57. pyossmtool/bundle/defaults/allowlists/acronyms.yaml +10 -0
  58. pyossmtool/bundle/defaults/allowlists/folder-breadth.txt +1 -0
  59. pyossmtool/bundle/defaults/allowlists/module-private-vars.txt +1 -0
  60. pyossmtool/bundle/defaults/allowlists/module-size.txt +1 -0
  61. pyossmtool/bundle/defaults/configs/bandit.yaml +5 -0
  62. pyossmtool/bundle/defaults/configs/gates/acronym-allowlist.yaml +4 -0
  63. pyossmtool/bundle/defaults/configs/gates/folder-breadth.yaml +12 -0
  64. pyossmtool/bundle/defaults/configs/gates/module-private-vars.yaml +4 -0
  65. pyossmtool/bundle/defaults/configs/gates/module-size.yaml +6 -0
  66. pyossmtool/bundle/defaults/configs/markdownlint.json +8 -0
  67. pyossmtool/bundle/defaults/configs/ruff.toml +8 -0
  68. pyossmtool/bundle/defaults/configs/ty.toml +5 -0
  69. pyossmtool/bundle/defaults/configs/yamlfmt.yaml +4 -0
  70. pyossmtool/bundle/defaults/configs/yamllint.yaml +12 -0
  71. pyossmtool/bundle/defaults/gates/README.md +150 -0
  72. pyossmtool/bundle/defaults/gates/acronym-allowlist.sh +15 -0
  73. pyossmtool/bundle/defaults/gates/folder-breadth.sh +22 -0
  74. pyossmtool/bundle/defaults/gates/lib.sh +116 -0
  75. pyossmtool/bundle/defaults/gates/module-private-vars.sh +77 -0
  76. pyossmtool/bundle/defaults/gates/module-size.sh +60 -0
  77. pyossmtool/bundle/defaults/semgrep/python-quality.yml +19 -0
  78. pyossmtool/bundle/suites/demo.yaml +11 -0
  79. pyossmtool/bundle/suites/extended.yaml +34 -0
  80. pyossmtool/bundle/suites/format.yaml +10 -0
  81. pyossmtool/bundle/suites/formatting.yaml +10 -0
  82. pyossmtool/bundle/suites/policy.yaml +11 -0
  83. pyossmtool/bundle/suites/python-quality.yaml +9 -0
  84. pyossmtool/bundle/suites/reslab-parity.yaml +41 -0
  85. pyossmtool/bundle/suites/standard.yaml +45 -0
  86. pyossmtool/cli.py +233 -0
  87. pyossmtool/config_resolver.py +117 -0
  88. pyossmtool/discovery.py +50 -0
  89. pyossmtool/gate_config.py +119 -0
  90. pyossmtool/gates.py +102 -0
  91. pyossmtool/ignore.py +270 -0
  92. pyossmtool/ignore_materialize.py +184 -0
  93. pyossmtool/installer.py +119 -0
  94. pyossmtool/models.py +279 -0
  95. pyossmtool/parsers/__init__.py +76 -0
  96. pyossmtool/parsers/analysis.py +329 -0
  97. pyossmtool/parsers/base.py +29 -0
  98. pyossmtool/parsers/common.py +57 -0
  99. pyossmtool/parsers/format_text.py +67 -0
  100. pyossmtool/parsers/gates.py +92 -0
  101. pyossmtool/parsers/patterns.py +78 -0
  102. pyossmtool/parsers/prose.py +193 -0
  103. pyossmtool/parsers/ruff.py +103 -0
  104. pyossmtool/parsers/shell.py +83 -0
  105. pyossmtool/policy/__init__.py +0 -0
  106. pyossmtool/policy/acronym_allowlist.py +322 -0
  107. pyossmtool/policy/folder_breadth.py +306 -0
  108. pyossmtool/registry.py +72 -0
  109. pyossmtool/reporter.py +87 -0
  110. pyossmtool/resolver.py +146 -0
  111. pyossmtool/runner.py +482 -0
  112. pyossmtool/runner_script.py +131 -0
  113. pyossmtool/target_expand.py +112 -0
  114. pyossmtool-0.0.1.dist-info/METADATA +94 -0
  115. pyossmtool-0.0.1.dist-info/RECORD +119 -0
  116. pyossmtool-0.0.1.dist-info/WHEEL +5 -0
  117. pyossmtool-0.0.1.dist-info/entry_points.txt +2 -0
  118. pyossmtool-0.0.1.dist-info/licenses/LICENSE +21 -0
  119. pyossmtool-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,184 @@
1
+ """Per-tool ignore pattern materialization driven by ToolDef.ignore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+
10
+ from pyossmtool.ignore import EffectiveIgnores, ToolIgnoreMaterial, _all_patterns, _merge_yaml_config
11
+ from pyossmtool.models import ExtendFrom, IgnoreKind, ToolDef, ToolIgnoreSpec
12
+
13
+
14
+ def materialize_for_tool(
15
+ tool: ToolDef,
16
+ ignores: EffectiveIgnores,
17
+ *,
18
+ project_root: Path,
19
+ check_id: str,
20
+ base_config_path: Path | None = None,
21
+ ) -> ToolIgnoreMaterial:
22
+ patterns = _all_patterns(ignores)
23
+ if not patterns or tool.ignore is None:
24
+ return ToolIgnoreMaterial()
25
+
26
+ cache_dir = project_root / ".pyossmtool" / "cache" / "ignores" / check_id
27
+ cache_dir.mkdir(parents=True, exist_ok=True)
28
+ return _materialize(tool.ignore, patterns, project_root, cache_dir, base_config_path)
29
+
30
+
31
+ def _materialize(
32
+ spec: ToolIgnoreSpec,
33
+ patterns: list[str],
34
+ project_root: Path,
35
+ cache_dir: Path,
36
+ base_config_path: Path | None,
37
+ ) -> ToolIgnoreMaterial:
38
+ if spec.kind == IgnoreKind.FLAG_PAIRS:
39
+ return _flag_pairs(spec, patterns)
40
+ if spec.kind == IgnoreKind.PREFIX:
41
+ return _prefix_flags(spec, patterns)
42
+ if spec.kind == IgnoreKind.SKIP_FILE:
43
+ return _skip_file(spec, patterns, cache_dir)
44
+ if spec.kind == IgnoreKind.CONFIG_OVERLAY:
45
+ return _config_overlay(spec, patterns, project_root, cache_dir, base_config_path)
46
+ return ToolIgnoreMaterial()
47
+
48
+
49
+ def _flag_pairs(spec: ToolIgnoreSpec, patterns: list[str]) -> ToolIgnoreMaterial:
50
+ if not spec.flag:
51
+ return ToolIgnoreMaterial()
52
+ argv: list[str] = []
53
+ for pattern in patterns:
54
+ argv.extend([spec.flag, pattern])
55
+ return ToolIgnoreMaterial(argv=argv, post_subcommand=spec.post_subcommand)
56
+
57
+
58
+ def _prefix_flags(spec: ToolIgnoreSpec, patterns: list[str]) -> ToolIgnoreMaterial:
59
+ if not spec.prefix:
60
+ return ToolIgnoreMaterial()
61
+ return ToolIgnoreMaterial(
62
+ argv=[f"{spec.prefix}{pattern}" for pattern in patterns],
63
+ post_subcommand=spec.post_subcommand,
64
+ )
65
+
66
+
67
+ def _skip_file(spec: ToolIgnoreSpec, patterns: list[str], cache_dir: Path) -> ToolIgnoreMaterial:
68
+ filename = spec.file or "ignores.skip"
69
+ skip_file = cache_dir / filename
70
+ skip_file.write_text("\n".join(patterns) + "\n", encoding="utf-8")
71
+ flag = spec.flag or "--skip"
72
+ return ToolIgnoreMaterial(
73
+ argv=[flag, str(skip_file)],
74
+ skip_file=skip_file,
75
+ post_subcommand=spec.post_subcommand,
76
+ )
77
+
78
+
79
+ def _config_overlay(
80
+ spec: ToolIgnoreSpec,
81
+ patterns: list[str],
82
+ project_root: Path,
83
+ cache_dir: Path,
84
+ base_config_path: Path | None,
85
+ ) -> ToolIgnoreMaterial:
86
+ filename = spec.file or f"ignores.{spec.format or 'txt'}"
87
+ config_path = cache_dir / filename
88
+ body = _overlay_body(spec, patterns, project_root, base_config_path)
89
+ if body is None:
90
+ return ToolIgnoreMaterial()
91
+ config_path.write_text(body, encoding="utf-8")
92
+ flag = spec.flag or "--config"
93
+ return ToolIgnoreMaterial(
94
+ argv=[flag, str(config_path)],
95
+ config_path=config_path,
96
+ post_subcommand=spec.post_subcommand,
97
+ )
98
+
99
+
100
+ def _overlay_body(
101
+ spec: ToolIgnoreSpec,
102
+ patterns: list[str],
103
+ project_root: Path,
104
+ base_config_path: Path | None,
105
+ ) -> str | None:
106
+ if spec.format == "toml":
107
+ return _toml_overlay(spec, patterns, project_root, base_config_path)
108
+ if spec.format == "yaml":
109
+ return _yaml_overlay(spec, patterns, base_config_path)
110
+ if spec.format == "json":
111
+ return _json_overlay(spec, patterns)
112
+ return None
113
+
114
+
115
+ def _toml_overlay(
116
+ spec: ToolIgnoreSpec,
117
+ patterns: list[str],
118
+ project_root: Path,
119
+ base_config_path: Path | None,
120
+ ) -> str:
121
+ lines: list[str] = []
122
+ extend_line = _toml_extend_line(spec, project_root, base_config_path)
123
+ if extend_line:
124
+ lines.append(extend_line)
125
+ if spec.toml_table:
126
+ lines.append(f"[{spec.toml_table}]")
127
+ key = spec.key or "exclude"
128
+ lines.append(f"{key} = [")
129
+ lines.extend(f' "{pattern}",' for pattern in patterns)
130
+ lines.append("]")
131
+ return "\n".join(lines) + "\n"
132
+
133
+
134
+ def _toml_extend_line(
135
+ spec: ToolIgnoreSpec,
136
+ project_root: Path,
137
+ base_config_path: Path | None,
138
+ ) -> str | None:
139
+ extend_key = spec.extend_key or "extend"
140
+ if spec.extend_from == ExtendFrom.BASE_CONFIG:
141
+ return _extend_base_only(extend_key, base_config_path)
142
+ if spec.extend_from == ExtendFrom.BASE_OR_PYPROJECT:
143
+ return _extend_base_or_pyproject(extend_key, project_root, base_config_path)
144
+ if spec.extend_from == ExtendFrom.PYPROJECT:
145
+ return _extend_pyproject_list(extend_key, project_root)
146
+ return None
147
+
148
+
149
+ def _extend_base_only(extend_key: str, base_config_path: Path | None) -> str | None:
150
+ if base_config_path and base_config_path.exists():
151
+ return f'{extend_key} = "{base_config_path}"'
152
+ return None
153
+
154
+
155
+ def _extend_base_or_pyproject(extend_key: str, project_root: Path, base_config_path: Path | None) -> str | None:
156
+ base = _extend_base_only(extend_key, base_config_path)
157
+ if base:
158
+ return base
159
+ pyproject = project_root / "pyproject.toml"
160
+ if pyproject.exists():
161
+ return f'{extend_key} = "{pyproject}"'
162
+ return None
163
+
164
+
165
+ def _extend_pyproject_list(extend_key: str, project_root: Path) -> str | None:
166
+ pyproject = project_root / "pyproject.toml"
167
+ if pyproject.exists():
168
+ return f'{extend_key} = ["{pyproject}"]'
169
+ return None
170
+
171
+
172
+ def _yaml_overlay(spec: ToolIgnoreSpec, patterns: list[str], base_config_path: Path | None) -> str:
173
+ key = spec.key or "exclude"
174
+ if spec.key_style == "block":
175
+ overlay = {key: "\n".join(patterns) + ("\n" if patterns else "")}
176
+ else:
177
+ overlay = {key: patterns}
178
+ merged = _merge_yaml_config(base_config_path, overlay)
179
+ return yaml.safe_dump(merged, sort_keys=False)
180
+
181
+
182
+ def _json_overlay(spec: ToolIgnoreSpec, patterns: list[str]) -> str:
183
+ key = spec.key or "ignore"
184
+ return json.dumps({key: patterns}, indent=2) + "\n"
@@ -0,0 +1,119 @@
1
+ """Install quality tool dependencies into managed environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from pyossmtool.models import EnvMode, InstallMethod, SuiteDef, ToolDef
11
+ from pyossmtool.registry import Registry
12
+ from pyossmtool.resolver import BinaryResolver
13
+
14
+
15
+ class Installer:
16
+ def __init__(self, registry: Registry, project_root: Path | None = None) -> None:
17
+ self.registry = registry
18
+ self.project_root = (project_root or Path.cwd()).resolve()
19
+ self.resolver = BinaryResolver(self.project_root)
20
+ self.managed_venv = self.project_root / ".pyossmtool" / "tools" / "venv"
21
+
22
+ def tools_for_suite(self, suite: SuiteDef) -> list[ToolDef]:
23
+ tool_ids: set[str] = set()
24
+ for check_ref in suite.checks:
25
+ check = self.registry.get_check(check_ref.id)
26
+ tool_ids.add(check.tool)
27
+ return [self.registry.get_tool(tool_id) for tool_id in sorted(tool_ids)]
28
+
29
+ def install_suite(self, suite_id: str) -> None:
30
+ suite = self.registry.get_suite(suite_id)
31
+ for tool in self.tools_for_suite(suite):
32
+ self.install_tool(tool)
33
+
34
+ def install_tool(self, tool: ToolDef) -> None:
35
+ method = tool.install.method
36
+ handlers = {
37
+ InstallMethod.SKIP: lambda _tool: None,
38
+ InstallMethod.SYSTEM: self._warn_missing_system_tool,
39
+ InstallMethod.PIP: self._install_pip_tool,
40
+ InstallMethod.NPM: self._install_npm_tool,
41
+ }
42
+ handler = handlers.get(method)
43
+ if handler is None:
44
+ raise RuntimeError(f"Unsupported install method: {method}")
45
+ handler(tool)
46
+
47
+ def _install_npm_tool(self, tool: ToolDef) -> None:
48
+ if self._binary_available(tool):
49
+ return
50
+ self._install_npm(tool)
51
+ self._require_binary(tool)
52
+
53
+ def _warn_missing_system_tool(self, tool: ToolDef) -> None:
54
+ if self.resolver._resolve_system(tool.binary) is not None:
55
+ return
56
+ print(
57
+ f"WARN: system tool '{tool.binary}' not found; install manually to enable related checks",
58
+ file=sys.stderr,
59
+ )
60
+
61
+ def _install_pip_tool(self, tool: ToolDef) -> None:
62
+ self._ensure_managed_venv()
63
+ self._install_pip(tool)
64
+ self._require_binary(tool)
65
+
66
+ def _require_binary(self, tool: ToolDef) -> None:
67
+ if self._binary_available(tool):
68
+ return
69
+ raise RuntimeError(f"Failed to install tool '{tool.id}' ({tool.binary})")
70
+
71
+ def _binary_available(self, tool: ToolDef) -> bool:
72
+ try:
73
+ self.resolver.resolve(tool, mode=EnvMode.MANAGED)
74
+ return True
75
+ except FileNotFoundError:
76
+ return False
77
+
78
+ def _managed_python(self) -> str:
79
+ if sys.version_info < (3, 14):
80
+ return sys.executable
81
+ for candidate in ("python3.13", "python3.12", "python3.11"):
82
+ found = shutil.which(candidate)
83
+ if found:
84
+ return found
85
+ return sys.executable
86
+
87
+ def _ensure_managed_venv(self) -> None:
88
+ python_bin = self.managed_venv / "bin" / "python"
89
+ if python_bin.exists():
90
+ version = subprocess.run(
91
+ [str(python_bin), "--version"],
92
+ capture_output=True,
93
+ text=True,
94
+ check=True,
95
+ ).stdout
96
+ if "3.14" in version:
97
+ shutil.rmtree(self.managed_venv)
98
+ if (self.managed_venv / "bin" / "python").exists():
99
+ return
100
+ self.managed_venv.parent.mkdir(parents=True, exist_ok=True)
101
+ subprocess.run([self._managed_python(), "-m", "venv", str(self.managed_venv)], check=True)
102
+
103
+ def _install_pip(self, tool: ToolDef) -> None:
104
+ pip = self.managed_venv / "bin" / "pip"
105
+ packages = (tool.install.package or tool.id).split()
106
+ subprocess.run([str(pip), "install", "--upgrade", *packages], check=True)
107
+
108
+ def _install_npm(self, tool: ToolDef) -> None:
109
+ package = tool.install.package or tool.id
110
+ npm_root = self.project_root / ".pyossmtool" / "tools" / "npm"
111
+ npm_root.mkdir(parents=True, exist_ok=True)
112
+ package_json = npm_root / "package.json"
113
+ if not package_json.exists():
114
+ package_json.write_text('{"name":"pyossmtool-tools","private":true}\n', encoding="utf-8")
115
+ subprocess.run(
116
+ ["npm", "install", "--no-save", "--prefix", str(npm_root), package],
117
+ check=True,
118
+ cwd=npm_root,
119
+ )
pyossmtool/models.py ADDED
@@ -0,0 +1,279 @@
1
+ """Pydantic models for tools, checks, suites, and failure reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
10
+
11
+ SCHEMA_VERSION = "1.0"
12
+
13
+
14
+ class InstallMethod(StrEnum):
15
+ PIP = "pip"
16
+ NPM = "npm"
17
+ SYSTEM = "system"
18
+ SKIP = "skip"
19
+
20
+
21
+ class EnvMode(StrEnum):
22
+ MANAGED = "managed"
23
+ PROJECT = "project"
24
+ AUTO = "auto"
25
+
26
+
27
+ class Severity(StrEnum):
28
+ ERROR = "error"
29
+ WARNING = "warning"
30
+ INFO = "info"
31
+
32
+
33
+ class InstallSpec(BaseModel):
34
+ method: InstallMethod
35
+ package: str | None = None
36
+ version: str | None = None
37
+
38
+
39
+ class ToolConfigSpec(BaseModel):
40
+ """Declarative config discovery for a tool (repo-native vs bundled)."""
41
+
42
+ flag: str | None = None
43
+ pass_flag: bool | None = None
44
+ bundled: str | None = None
45
+ bundled_path: str | None = None
46
+ repo_files: list[str] = Field(default_factory=list)
47
+ repo_dirs: list[str] = Field(default_factory=list)
48
+ pyproject: list[str] = Field(default_factory=list)
49
+ use_pyproject_as_path: bool = False
50
+
51
+ def should_pass_flag(self) -> bool:
52
+ if self.pass_flag is not None:
53
+ return self.pass_flag
54
+ return bool(self.flag)
55
+
56
+
57
+ class IgnoreKind(StrEnum):
58
+ FLAG_PAIRS = "flag_pairs"
59
+ PREFIX = "prefix"
60
+ SKIP_FILE = "skip_file"
61
+ CONFIG_OVERLAY = "config_overlay"
62
+
63
+
64
+ class ExtendFrom(StrEnum):
65
+ NONE = "none"
66
+ BASE_CONFIG = "base_config"
67
+ BASE_OR_PYPROJECT = "base_or_pyproject"
68
+ PYPROJECT = "pyproject"
69
+
70
+
71
+ class ToolIgnoreSpec(BaseModel):
72
+ """How this tool consumes unified ignore patterns."""
73
+
74
+ kind: IgnoreKind
75
+ flag: str | None = None
76
+ prefix: str | None = None
77
+ post_subcommand: bool = False
78
+ format: str | None = None
79
+ file: str | None = None
80
+ key: str | None = None
81
+ key_style: str = "list"
82
+ extend_from: ExtendFrom = ExtendFrom.NONE
83
+ extend_key: str | None = None
84
+ toml_table: str | None = None
85
+
86
+
87
+ class ToolDef(BaseModel):
88
+ id: str
89
+ name: str
90
+ description: str
91
+ install: InstallSpec
92
+ binary: str
93
+ documentation_url: str | None = None
94
+ files: list[str] = Field(default_factory=list)
95
+ config: ToolConfigSpec | None = None
96
+ ignore: ToolIgnoreSpec | None = None
97
+
98
+
99
+ class SuccessCriteria(BaseModel):
100
+ exit_codes: list[int] = Field(default_factory=lambda: [0])
101
+
102
+
103
+ class CheckPolicy(BaseModel):
104
+ max_complexity_rank: str | None = None
105
+ max_duplication_percent: float | None = None
106
+ min_coverage_percent: float | None = None
107
+ coverage_source: str | None = None
108
+ min_confidence: int | None = None
109
+
110
+
111
+ class ConfigMode(StrEnum):
112
+ AUTO = "auto"
113
+ BUNDLED = "bundled"
114
+ PATHS = "paths"
115
+
116
+
117
+ class CheckMode(StrEnum):
118
+ CHECK = "check"
119
+ FORMAT = "format"
120
+
121
+
122
+ class ConfigSpec(BaseModel):
123
+ mode: ConfigMode = ConfigMode.AUTO
124
+ paths: dict[str, str] = Field(default_factory=dict)
125
+
126
+
127
+ class IgnoreSpec(BaseModel):
128
+ """Repo-wide ignore rules merged across suite, project, and per-check layers."""
129
+
130
+ model_config = ConfigDict(populate_by_name=True)
131
+
132
+ ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
133
+ ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
134
+
135
+
136
+ class GateConfigSpec(BaseModel):
137
+ """Declarative policy-config discovery for script gates."""
138
+
139
+ bundled: str | None = None
140
+ project_file: str | None = None
141
+ allowlist_bundled: str | None = None
142
+
143
+
144
+ class CheckDef(BaseModel):
145
+ model_config = ConfigDict(populate_by_name=True)
146
+
147
+ id: str
148
+ tool: str
149
+ name: str
150
+ description: str
151
+ argv: list[str] = Field(default_factory=list)
152
+ parser: str
153
+ mode: CheckMode = CheckMode.CHECK
154
+ script: str | None = None
155
+ include: list[str] = Field(default_factory=list)
156
+ config: GateConfigSpec | None = None
157
+ success: SuccessCriteria = Field(default_factory=SuccessCriteria)
158
+ policy: CheckPolicy | None = None
159
+ remediation: dict[str, Any] = Field(default_factory=dict)
160
+ output_file: str | None = None
161
+ ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
162
+ ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
163
+
164
+ @model_validator(mode="after")
165
+ def validate_script_gate(self) -> CheckDef:
166
+ if self.tool == "script" and not self.script:
167
+ raise ValueError(f"Check '{self.id}' uses tool 'script' but has no script path")
168
+ return self
169
+
170
+
171
+ class SuiteCheckRef(BaseModel):
172
+ model_config = ConfigDict(populate_by_name=True)
173
+
174
+ id: str
175
+ target: str | None = None
176
+ ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
177
+ ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
178
+
179
+
180
+ class SuiteDef(BaseModel):
181
+ model_config = ConfigDict(populate_by_name=True)
182
+
183
+ id: str
184
+ name: str
185
+ description: str
186
+ env: EnvMode = EnvMode.AUTO
187
+ checks: list[SuiteCheckRef]
188
+ target: str = "."
189
+ ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
190
+ ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
191
+
192
+
193
+ class ProjectConfig(BaseModel):
194
+ model_config = ConfigDict(populate_by_name=True)
195
+
196
+ suite: str
197
+ env: EnvMode = EnvMode.AUTO
198
+ target: str = "."
199
+ checks: list[SuiteCheckRef] = Field(default_factory=list)
200
+ configs: ConfigSpec = Field(default_factory=ConfigSpec)
201
+ ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
202
+ ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
203
+
204
+
205
+ class Location(BaseModel):
206
+ file: str
207
+ line: int | None = None
208
+ column: int | None = None
209
+ end_line: int | None = None
210
+ end_column: int | None = None
211
+
212
+
213
+ class FixHint(BaseModel):
214
+ description: str | None = None
215
+ command: str | None = None
216
+
217
+
218
+ class Finding(BaseModel):
219
+ rule_id: str
220
+ severity: Severity
221
+ message: str
222
+ location: Location | None = None
223
+ snippet: str | None = None
224
+ fix: FixHint | None = None
225
+
226
+
227
+ class FailureSummary(BaseModel):
228
+ finding_count: int
229
+ by_severity: dict[str, int] = Field(default_factory=dict)
230
+
231
+
232
+ class Remediation(BaseModel):
233
+ docs_url: str | None = None
234
+ suggested_commands: list[str] = Field(default_factory=list)
235
+
236
+
237
+ class FailureArtifacts(BaseModel):
238
+ raw_stdout: str | None = None
239
+ raw_stderr: str | None = None
240
+
241
+
242
+ class FailureReport(BaseModel):
243
+ schema_version: str = SCHEMA_VERSION
244
+ check_id: str
245
+ tool_id: str
246
+ status: str = "failed"
247
+ suite_id: str | None = None
248
+ target: str
249
+ started_at: datetime
250
+ duration_ms: int
251
+ summary: FailureSummary
252
+ findings: list[Finding]
253
+ remediation: Remediation = Field(default_factory=Remediation)
254
+ artifacts: FailureArtifacts = Field(default_factory=FailureArtifacts)
255
+
256
+
257
+ class CheckResult(BaseModel):
258
+ check_id: str
259
+ passed: bool
260
+ report_path: str | None = None
261
+ error: str | None = None
262
+
263
+
264
+ class SuiteResult(BaseModel):
265
+ suite_id: str
266
+ passed: bool
267
+ results: list[CheckResult] = Field(default_factory=list)
268
+
269
+
270
+ def utc_now() -> datetime:
271
+ return datetime.now(UTC)
272
+
273
+
274
+ def summarize_findings(findings: list[Finding]) -> FailureSummary:
275
+ by_severity: dict[str, int] = {}
276
+ for finding in findings:
277
+ key = finding.severity.value
278
+ by_severity[key] = by_severity.get(key, 0) + 1
279
+ return FailureSummary(finding_count=len(findings), by_severity=by_severity)
@@ -0,0 +1,76 @@
1
+ """Parse native tool output into normalized Finding objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyossmtool.models import CheckDef, Finding
6
+ from pyossmtool.parsers.analysis import (
7
+ BanditParser,
8
+ DeadcodeParser,
9
+ JscpdParser,
10
+ PydepsCyclesParser,
11
+ PytestParser,
12
+ RadonCcParser,
13
+ RadonMiParser,
14
+ SemgrepParser,
15
+ TyParser,
16
+ VultureParser,
17
+ )
18
+ from pyossmtool.parsers.base import REGISTRY
19
+ from pyossmtool.parsers.format_text import MdformatParser, YamlfmtParser
20
+ from pyossmtool.parsers.gates import CliTextParser, GateJsonParser, NoopParser, ScriptTextParser
21
+ from pyossmtool.parsers.prose import (
22
+ CodespellParser,
23
+ GitleaksParser,
24
+ HadolintParser,
25
+ MarkdownlintParser,
26
+ MutmutParser,
27
+ SourceryParser,
28
+ YamllintParser,
29
+ )
30
+ from pyossmtool.parsers.ruff import RuffFormatParser, RuffJsonParser
31
+ from pyossmtool.parsers.shell import ShellcheckParser, ShfmtDiffParser
32
+
33
+ # Keep registered classes referenced for static dead-code analysis.
34
+ _PARSER_CLASSES = (
35
+ BanditParser,
36
+ CliTextParser,
37
+ CodespellParser,
38
+ DeadcodeParser,
39
+ GateJsonParser,
40
+ GitleaksParser,
41
+ HadolintParser,
42
+ JscpdParser,
43
+ MarkdownlintParser,
44
+ MdformatParser,
45
+ MutmutParser,
46
+ NoopParser,
47
+ PydepsCyclesParser,
48
+ PytestParser,
49
+ RadonCcParser,
50
+ RadonMiParser,
51
+ RuffFormatParser,
52
+ RuffJsonParser,
53
+ ScriptTextParser,
54
+ SemgrepParser,
55
+ ShellcheckParser,
56
+ ShfmtDiffParser,
57
+ SourceryParser,
58
+ TyParser,
59
+ VultureParser,
60
+ YamllintParser,
61
+ YamlfmtParser,
62
+ )
63
+
64
+
65
+ def parse_output(check: CheckDef, stdout: str, stderr: str) -> list[Finding]:
66
+ try:
67
+ parser_cls = REGISTRY[check.parser]
68
+ except KeyError as exc:
69
+ raise ValueError(f"Unknown parser: {check.parser}") from exc
70
+ parser = parser_cls()
71
+ if parser_cls.needs_check:
72
+ return parser.parse(stdout, stderr, check=check)
73
+ return parser.parse(stdout, stderr)
74
+
75
+
76
+ __all__ = ["REGISTRY", "parse_output", "_PARSER_CLASSES"]