code-standards 7.0.0__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 (99) hide show
  1. code_standards-7.0.0.dist-info/METADATA +53 -0
  2. code_standards-7.0.0.dist-info/RECORD +99 -0
  3. code_standards-7.0.0.dist-info/WHEEL +4 -0
  4. code_standards-7.0.0.dist-info/entry_points.txt +3 -0
  5. code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
  6. sarj_standards/__init__.py +30 -0
  7. sarj_standards/__main__.py +5 -0
  8. sarj_standards/_meta.py +22 -0
  9. sarj_standards/api.py +890 -0
  10. sarj_standards/cli/__init__.py +0 -0
  11. sarj_standards/cli/main.py +2466 -0
  12. sarj_standards/configs/cli-reference.v1.json +1 -0
  13. sarj_standards/configs/doctor.config.json +22 -0
  14. sarj_standards/configs/eslint.application.mjs +1366 -0
  15. sarj_standards/configs/eslint.peers.json +44 -0
  16. sarj_standards/configs/eslint.strict.mjs +1060 -0
  17. sarj_standards/configs/markdownlint.strict.yaml +12 -0
  18. sarj_standards/configs/pyright.strict.json +96 -0
  19. sarj_standards/configs/ruff.application.toml +363 -0
  20. sarj_standards/configs/ruff.strict.toml +338 -0
  21. sarj_standards/configs/rule-inventory.v1.json +1 -0
  22. sarj_standards/configs/rule-ledger.json +846 -0
  23. sarj_standards/configs/rule-warning-levels.v1.json +1 -0
  24. sarj_standards/configs/taplo.strict.toml +14 -0
  25. sarj_standards/configs/yamllint.strict.yaml +25 -0
  26. sarj_standards/libs/__init__.py +0 -0
  27. sarj_standards/libs/adoption/__init__.py +0 -0
  28. sarj_standards/libs/adoption/configs.py +36 -0
  29. sarj_standards/libs/adoption/doctor.py +1346 -0
  30. sarj_standards/libs/adoption/exclusions.py +66 -0
  31. sarj_standards/libs/adoption/hooks.py +423 -0
  32. sarj_standards/libs/adoption/launcher.py +240 -0
  33. sarj_standards/libs/adoption/lifecycle.py +493 -0
  34. sarj_standards/libs/adoption/manifest.py +550 -0
  35. sarj_standards/libs/adoption/packagemanager.py +285 -0
  36. sarj_standards/libs/adoption/retired_suppressions.py +371 -0
  37. sarj_standards/libs/adoption/scaffold.py +1660 -0
  38. sarj_standards/libs/adoption/service.py +441 -0
  39. sarj_standards/libs/adoption/transaction.py +274 -0
  40. sarj_standards/libs/adoption/upgrade.py +516 -0
  41. sarj_standards/libs/adoption/uvtool.py +62 -0
  42. sarj_standards/libs/catalogs/__init__.py +9 -0
  43. sarj_standards/libs/catalogs/slack_automations.py +627 -0
  44. sarj_standards/libs/corpus/__init__.py +25 -0
  45. sarj_standards/libs/corpus/manifest.py +211 -0
  46. sarj_standards/libs/corpus/snapshot.py +222 -0
  47. sarj_standards/libs/diagnostics/__init__.py +65 -0
  48. sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
  49. sarj_standards/libs/diagnostics/baseline.py +131 -0
  50. sarj_standards/libs/diagnostics/models.py +574 -0
  51. sarj_standards/libs/diagnostics/serialize.py +290 -0
  52. sarj_standards/libs/diagnostics/source.py +172 -0
  53. sarj_standards/libs/filesystem.py +11 -0
  54. sarj_standards/libs/linting/__init__.py +0 -0
  55. sarj_standards/libs/linting/analysis.py +422 -0
  56. sarj_standards/libs/linting/external.py +1454 -0
  57. sarj_standards/libs/linting/library_policy.py +688 -0
  58. sarj_standards/libs/linting/policy.py +152 -0
  59. sarj_standards/libs/linting/runner.py +442 -0
  60. sarj_standards/libs/linting/textlint.py +1605 -0
  61. sarj_standards/libs/release/__init__.py +98 -0
  62. sarj_standards/libs/release/_values.py +24 -0
  63. sarj_standards/libs/release/artifacts.py +191 -0
  64. sarj_standards/libs/release/causality.py +80 -0
  65. sarj_standards/libs/release/changes.py +48 -0
  66. sarj_standards/libs/release/process.py +128 -0
  67. sarj_standards/libs/release/publish.py +85 -0
  68. sarj_standards/libs/release/registry.py +271 -0
  69. sarj_standards/libs/release/release_age.py +218 -0
  70. sarj_standards/libs/release/rollout.py +1163 -0
  71. sarj_standards/libs/release/tags.py +373 -0
  72. sarj_standards/libs/release/typescript.py +191 -0
  73. sarj_standards/libs/repository/__init__.py +0 -0
  74. sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
  75. sarj_standards/libs/repository/comment_corpus.py +536 -0
  76. sarj_standards/libs/repository/config_generation.py +146 -0
  77. sarj_standards/libs/repository/docs.py +347 -0
  78. sarj_standards/libs/repository/hooks.py +118 -0
  79. sarj_standards/libs/repository/ledger.py +99 -0
  80. sarj_standards/libs/repository/repository.py +744 -0
  81. sarj_standards/libs/repository/rule_authoring.py +246 -0
  82. sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
  83. sarj_standards/libs/repository/rule_changes.py +318 -0
  84. sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
  85. sarj_standards/libs/repository/rule_lifecycle.py +167 -0
  86. sarj_standards/libs/repository/rule_maintenance.py +225 -0
  87. sarj_standards/libs/rules/__init__.py +74 -0
  88. sarj_standards/libs/rules/catalog.py +145 -0
  89. sarj_standards/libs/rules/contracts.py +382 -0
  90. sarj_standards/libs/rules/corpus_runner.py +365 -0
  91. sarj_standards/libs/rules/evaluation.py +177 -0
  92. sarj_standards/libs/setup/__init__.py +4 -0
  93. sarj_standards/libs/setup/repository.py +40 -0
  94. sarj_standards/py.typed +0 -0
  95. sarj_standards/schemas/__init__.py +4 -0
  96. sarj_standards/schemas/_paths.py +7 -0
  97. sarj_standards/schemas/rule-catalog.v1.json +1 -0
  98. sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
  99. sarj_standards/schemas/slack-automations.v1.schema.json +1751 -0
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import lru_cache
5
+ import json
6
+ from pathlib import Path
7
+ from types import MappingProxyType
8
+ from typing import TYPE_CHECKING, Final, Self
9
+
10
+ from pathspec import PathSpec
11
+ from pathspec.pattern import Pattern
12
+
13
+ from sarj_standards._meta import CONFIGS_DIR
14
+ from sarj_standards.libs.adoption.manifest import MANIFEST_NAME, ExclusionOverride, Manifest
15
+ from sarj_standards.libs.rules import RuleSelector
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Iterable, Mapping
20
+
21
+ from sarj_standards.libs.diagnostics import Diagnostic
22
+
23
+
24
+ _SOURCE_ENGINES: Final[Mapping[str, str]] = MappingProxyType(
25
+ {
26
+ "sarj-python-lint": "python",
27
+ "sarj-sql-lint": "sql",
28
+ "sarj-iac-lint": "iac",
29
+ "sarj-text-lint": "text",
30
+ "sarj-library-policy": "python",
31
+ "ruff": "ruff",
32
+ "basedpyright": "basedpyright",
33
+ "eslint": "eslint",
34
+ }
35
+ )
36
+ _NON_EXCLUDABLE_CODE: Final = "SARJ206"
37
+ _TERRAFORM_TEST_SUFFIXES: Final = (".tftest.hcl", ".tftest.json")
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class _Override:
42
+ paths: PathSpec[Pattern]
43
+ rules: frozenset[str]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Policy:
48
+ root: Path
49
+ excluded_paths: PathSpec[Pattern]
50
+ excluded_rules: frozenset[str]
51
+ overrides: tuple[_Override, ...]
52
+
53
+ @classmethod
54
+ def from_manifest(cls, root: Path, manifest: Manifest | None) -> Self:
55
+ resolved = root.resolve()
56
+ if manifest is None:
57
+ return cls(resolved, PathSpec.from_lines("gitignore", ()), frozenset(), ())
58
+ return cls(
59
+ resolved,
60
+ PathSpec.from_lines("gitignore", manifest.excluded_paths),
61
+ frozenset(manifest.excluded_rules),
62
+ tuple(_compile_override(item) for item in manifest.exclusion_overrides),
63
+ )
64
+
65
+ @classmethod
66
+ def corpus_from_manifest(cls, root: Path, manifest: Manifest | None) -> Self:
67
+ policy = cls.from_manifest(root, manifest)
68
+ return cls(policy.root, policy.excluded_paths, frozenset(), ())
69
+
70
+ @classmethod
71
+ def observe_from_manifest(cls, root: Path, manifest: Manifest | None) -> Self:
72
+ resolved = root.resolve()
73
+ if manifest is None:
74
+ return cls(resolved, PathSpec.from_lines("gitignore", ()), frozenset(), ())
75
+ return cls(
76
+ resolved,
77
+ PathSpec.from_lines("gitignore", manifest.excluded_paths),
78
+ frozenset(manifest.excluded_rules),
79
+ tuple(_compile_override(item) for item in manifest.exclusion_overrides),
80
+ )
81
+
82
+ def allows_path(self, path: str | Path) -> bool:
83
+ relative = self.relative(path)
84
+ if relative == MANIFEST_NAME:
85
+ return True
86
+ if relative.casefold().endswith(_TERRAFORM_TEST_SUFFIXES):
87
+ return True
88
+ return not self.excluded_paths.match_file(relative)
89
+
90
+ def allows_rule(self, diagnostic: Diagnostic) -> bool:
91
+ if diagnostic.code == _NON_EXCLUDABLE_CODE:
92
+ return True
93
+ engine = _SOURCE_ENGINES.get(diagnostic.source, diagnostic.source)
94
+ selectors = frozenset(
95
+ {
96
+ f"{engine}:{diagnostic.code}",
97
+ f"{engine}:{diagnostic.rule_id}",
98
+ }
99
+ if diagnostic.rule_id is not None
100
+ else {f"{engine}:{diagnostic.code}"}
101
+ )
102
+ if selectors & self.excluded_rules:
103
+ return False
104
+ relative = diagnostic.location.path
105
+ return not any(
106
+ override.paths.match_file(relative) and selectors & override.rules for override in self.overrides
107
+ )
108
+
109
+ def filter_paths(self, paths: Iterable[str]) -> tuple[str, ...]:
110
+ return tuple(path for path in paths if self.allows_path(path))
111
+
112
+ def filter_diagnostics(self, diagnostics: Iterable[Diagnostic]) -> tuple[Diagnostic, ...]:
113
+ return tuple(item for item in diagnostics if self.allows_path(item.location.path) and self.allows_rule(item))
114
+
115
+ def relative(self, path: str | Path) -> str:
116
+ candidate = Path(path)
117
+ resolved = (candidate if candidate.is_absolute() else self.root / candidate).resolve()
118
+ try:
119
+ return resolved.relative_to(self.root).as_posix()
120
+ except ValueError as exc:
121
+ msg = f"policy path is outside repository root: {path}"
122
+ raise ValueError(msg) from exc
123
+
124
+
125
+ def _compile_override(value: ExclusionOverride) -> _Override:
126
+ return _Override(PathSpec.from_lines("gitignore", value.paths), frozenset(value.rules))
127
+
128
+
129
+ @lru_cache(maxsize=1)
130
+ def warning_selectors() -> frozenset[RuleSelector]:
131
+ payload: object = json.loads( # pyright: ignore[reportAny]
132
+ (CONFIGS_DIR / "rule-warning-levels.v1.json").read_text(encoding="utf-8")
133
+ )
134
+ if not isinstance(payload, dict):
135
+ msg = "invalid bundled warning-rule lifecycle"
136
+ raise TypeError(msg)
137
+ document: dict[str, object] = payload # pyright: ignore[reportUnknownVariableType]
138
+ if document.get("schemaVersion") != 1:
139
+ msg = "invalid bundled warning-rule lifecycle"
140
+ raise ValueError(msg)
141
+ rules_value = document.get("rules")
142
+ if not isinstance(rules_value, list):
143
+ msg = "invalid bundled warning-rule selectors"
144
+ raise TypeError(msg)
145
+ rules: list[object] = rules_value # pyright: ignore[reportUnknownVariableType]
146
+ if any(not isinstance(value, str) for value in rules):
147
+ msg = "invalid bundled warning-rule selectors"
148
+ raise TypeError(msg)
149
+ return frozenset(RuleSelector.parse(value) for value in rules if isinstance(value, str))
150
+
151
+
152
+ __all__ = ["Policy"]
@@ -0,0 +1,442 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from dataclasses import dataclass, field
5
+ from enum import StrEnum
6
+ from importlib import import_module
7
+ from itertools import pairwise
8
+ import os
9
+ from pathlib import Path
10
+ import stat
11
+ from types import MappingProxyType
12
+ from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
13
+
14
+ from sarj_standards.libs.filesystem import is_link_like
15
+
16
+ from . import textlint
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Iterable, Sequence
21
+
22
+ from .policy import Policy
23
+
24
+
25
+ class _Rule(Protocol):
26
+ """Registry value shape shared by the three standards linters."""
27
+
28
+
29
+ @runtime_checkable
30
+ class _CheckerModule(Protocol):
31
+ def main(self, argv: list[str]) -> int: ...
32
+
33
+
34
+ @runtime_checkable
35
+ class _RegistryModule(Protocol):
36
+ REGISTRY: Mapping[str, type[_Rule]]
37
+
38
+
39
+ class _LoadedTool(NamedTuple):
40
+ checker: Callable[[list[str]], int]
41
+ registry: Mapping[str, type[_Rule]]
42
+
43
+
44
+ class _Tool(StrEnum):
45
+ PYTHON = "python"
46
+ SQL = "sql"
47
+ IAC = "iac"
48
+ TEXT = "text"
49
+ TYPESCRIPT = "typescript"
50
+
51
+
52
+ _SUFFIX_TO_TOOL = MappingProxyType(
53
+ {
54
+ ".hcl": _Tool.IAC,
55
+ ".py": _Tool.PYTHON,
56
+ ".sql": _Tool.SQL,
57
+ ".tf": _Tool.IAC,
58
+ ".tfvars": _Tool.IAC,
59
+ ".cjs": _Tool.TYPESCRIPT,
60
+ ".cts": _Tool.TYPESCRIPT,
61
+ ".js": _Tool.TYPESCRIPT,
62
+ ".jsx": _Tool.TYPESCRIPT,
63
+ ".mjs": _Tool.TYPESCRIPT,
64
+ ".mts": _Tool.TYPESCRIPT,
65
+ ".ts": _Tool.TYPESCRIPT,
66
+ ".tsx": _Tool.TYPESCRIPT,
67
+ ".yaml": _Tool.IAC,
68
+ ".yml": _Tool.IAC,
69
+ }
70
+ )
71
+ _TERRAFORM_TEST_SUFFIXES = (".tftest.hcl", ".tftest.json")
72
+ _IGNORED_DIRS = frozenset(
73
+ {
74
+ ".build",
75
+ ".astro",
76
+ ".cache",
77
+ ".coverage",
78
+ ".gradle",
79
+ ".hypothesis",
80
+ ".git",
81
+ ".mypy_cache",
82
+ ".next",
83
+ ".open-next",
84
+ ".nox",
85
+ ".nuxt",
86
+ ".output",
87
+ ".parcel-cache",
88
+ ".playwright-mcp",
89
+ ".pnpm-store",
90
+ ".pytest_cache",
91
+ ".pytype",
92
+ ".ruff_cache",
93
+ ".svelte-kit",
94
+ ".terraform",
95
+ ".turbo",
96
+ ".uv-cache",
97
+ ".venv",
98
+ ".vite",
99
+ ".wrangler",
100
+ ".yarn",
101
+ ".tox",
102
+ "__pycache__",
103
+ "__generated__",
104
+ "_backups",
105
+ "dist",
106
+ "build",
107
+ "cache",
108
+ "caches",
109
+ "coverage",
110
+ "htmlcov",
111
+ "generated",
112
+ "node_modules",
113
+ "out",
114
+ "storybook-static",
115
+ "target",
116
+ "vendor",
117
+ "venv",
118
+ }
119
+ )
120
+ _MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024
121
+ _MAX_MARKDOWN_FILE_BYTES = 16 * 1024 * 1024
122
+ _IGNORED_DISCOVERED_FILES = frozenset({".env", ".env.mcp"})
123
+ _SKILL_ARTIFACT_ROOTS = frozenset({".agents", ".claude"})
124
+ _PYTHON_NOISE_RULES = frozenset(
125
+ {
126
+ "docstring-args-restate-signature",
127
+ "docstring-returns-restate-signature",
128
+ "duplicated-override-docstring",
129
+ "no-comment-cruft",
130
+ "no-long-comment",
131
+ "no-restated-comment",
132
+ "no-typed-doc-sections",
133
+ "no-unnecessary-docstring",
134
+ "no-vague-suppression-description",
135
+ "prefer-self-documenting-constant",
136
+ "redundant-class-docstring",
137
+ "redundant-docstring",
138
+ "redundant-module-docstring",
139
+ "restated-test-docstring",
140
+ "test-phase-label-comment",
141
+ "trailing-value-narration",
142
+ }
143
+ )
144
+ _SQL_NOISE_RULES = frozenset({"no-comment-cruft"})
145
+ _IAC_NOISE_RULES = frozenset({"no-comment-cruft", "no-restated-comment"})
146
+
147
+
148
+ @dataclass
149
+ class GroupedPaths:
150
+ python: list[str] = field(default_factory=list)
151
+ sql: list[str] = field(default_factory=list)
152
+ iac: list[str] = field(default_factory=list)
153
+ text: list[str] = field(default_factory=list)
154
+ typescript: list[str] = field(default_factory=list)
155
+
156
+
157
+ def run(
158
+ files: Sequence[str],
159
+ *,
160
+ noise_only: bool = False,
161
+ python_baseline: str | None = None,
162
+ policy: Policy | None = None,
163
+ ) -> int:
164
+ grouped = group_paths(files, policy=policy)
165
+ statuses = [
166
+ _run_tool(
167
+ "sarj_python_lint",
168
+ grouped.python,
169
+ selected=_PYTHON_NOISE_RULES if noise_only else None,
170
+ extra_args=("--baseline", python_baseline) if python_baseline is not None else (),
171
+ ),
172
+ _run_tool(
173
+ "sarj_sql_lint",
174
+ grouped.sql,
175
+ selected=_SQL_NOISE_RULES if noise_only else None,
176
+ ),
177
+ _run_tool(
178
+ "sarj_iac_lint",
179
+ grouped.iac,
180
+ selected=_IAC_NOISE_RULES if noise_only else None,
181
+ ),
182
+ ]
183
+ if grouped.text:
184
+ statuses.append(textlint.run(grouped.text))
185
+ return max(statuses)
186
+
187
+
188
+ def create_python_baseline(files: Sequence[str], output: str, *, policy: Policy | None = None) -> int:
189
+ grouped = group_paths(files, policy=policy)
190
+ return _run_tool(
191
+ "sarj_python_lint",
192
+ grouped.python,
193
+ selected=None,
194
+ extra_args=("--update-baseline", output),
195
+ )
196
+
197
+
198
+ def _run_tool(
199
+ package: str,
200
+ files: Sequence[str],
201
+ *,
202
+ selected: frozenset[str] | None,
203
+ extra_args: Sequence[str] = (),
204
+ ) -> int:
205
+ if not files or selected == frozenset():
206
+ return 0
207
+ checker, registry = _load_tool(package)
208
+ if selected is not None:
209
+ registry = _select_rules(registry, selected)
210
+ return _run(checker, registry, files, extra_args=extra_args)
211
+
212
+
213
+ def _select_rules(registry: Mapping[str, type[_Rule]], selected: frozenset[str]) -> dict[str, type[_Rule]]:
214
+ return {rule_id: rule for rule_id, rule in registry.items() if rule_id in selected}
215
+
216
+
217
+ def _load_tool(
218
+ package: str,
219
+ ) -> _LoadedTool:
220
+ checker_module = import_module(f"{package}.__main__")
221
+ registry_module = import_module(f"{package}.rules")
222
+ if not isinstance(checker_module, _CheckerModule) or not isinstance(registry_module, _RegistryModule):
223
+ msg = f"{package} does not expose the expected lint API"
224
+ raise TypeError(msg)
225
+ return _LoadedTool(checker_module.main, registry_module.REGISTRY)
226
+
227
+
228
+ def group_paths(files: Sequence[str], *, policy: Policy | None = None) -> GroupedPaths:
229
+ grouped = GroupedPaths()
230
+ inputs: list[tuple[str, Path, bool]] = []
231
+ for raw_path in files:
232
+ path = Path(raw_path)
233
+ if is_link_like(path):
234
+ msg = f"refusing symlink input: {raw_path}"
235
+ raise ValueError(msg)
236
+ if not path.exists():
237
+ msg = f"input does not exist: {raw_path}"
238
+ raise ValueError(msg)
239
+ if _is_skill_artifact(path):
240
+ continue
241
+ if path.is_file() and _owns_path(path):
242
+ if _is_conventionally_generated(path) or _has_generated_header(path):
243
+ continue
244
+ _validate_source_file(path, raw_path)
245
+ if policy is not None and not policy.allows_path(path):
246
+ continue
247
+ inputs.append((raw_path, path, path.is_dir()))
248
+
249
+ roots = _minimal_roots(path for _raw, path, is_directory in inputs if is_directory)
250
+ walked: set[Path] = set()
251
+ seen: set[Path] = set()
252
+ for raw_path, path, is_directory in inputs:
253
+ if is_directory:
254
+ key = _path_key(path)
255
+ if key in roots and key not in walked:
256
+ _route_directory(grouped, path, seen, policy=policy)
257
+ walked.add(key)
258
+ continue
259
+ _route_unique_path(grouped, path, raw_path, seen)
260
+ return grouped
261
+
262
+
263
+ def accepts_hook_path(path: Path) -> bool:
264
+ return (
265
+ _owns_path(path)
266
+ and not _is_skill_artifact(path)
267
+ and not _is_conventionally_generated(path)
268
+ and not _has_generated_header(path)
269
+ )
270
+
271
+
272
+ def _minimal_roots(paths: Iterable[Path]) -> frozenset[Path]:
273
+ resolved = sorted({_path_key(path) for path in paths}, key=lambda path: (len(path.parts), str(path)))
274
+ roots: list[Path] = []
275
+ for path in resolved:
276
+ if any(path.is_relative_to(root) for root in roots):
277
+ continue
278
+ roots.append(path)
279
+ return frozenset(roots)
280
+
281
+
282
+ def _route_directory(grouped: GroupedPaths, path: Path, seen: set[Path], *, policy: Policy | None = None) -> None:
283
+ for root, dir_names, file_names in os.walk(path, topdown=True, followlinks=False):
284
+ base = Path(root)
285
+ dir_names[:] = sorted(
286
+ name
287
+ for name in dir_names
288
+ if name not in _IGNORED_DIRS and not (base.name in _SKILL_ARTIFACT_ROOTS and name == "skills")
289
+ )
290
+ for file_name in sorted(file_names):
291
+ if file_name in _IGNORED_DISCOVERED_FILES:
292
+ continue
293
+ child = Path(root, file_name)
294
+ if not _owns_path(child):
295
+ continue
296
+ if _is_conventionally_generated(child):
297
+ continue
298
+ if policy is not None and not policy.allows_path(child):
299
+ continue
300
+ if not _is_routable_discovered_file(child):
301
+ continue
302
+ _route_unique_path(grouped, child, str(child), seen)
303
+
304
+
305
+ def _is_routable_discovered_file(path: Path) -> bool:
306
+ try:
307
+ metadata = path.stat(follow_symlinks=False)
308
+ if not stat.S_ISREG(metadata.st_mode):
309
+ return False
310
+ _validate_source_size(path, metadata.st_size)
311
+ except ValueError:
312
+ if _has_generated_header(path):
313
+ return False
314
+ raise
315
+ except OSError:
316
+ return False
317
+ return True
318
+
319
+
320
+ def _validate_source_file(path: Path, display: str) -> None:
321
+ try:
322
+ metadata = path.stat(follow_symlinks=False)
323
+ except OSError as exc:
324
+ msg = f"cannot inspect source input: {display}"
325
+ raise ValueError(msg) from exc
326
+ if not stat.S_ISREG(metadata.st_mode):
327
+ msg = f"source input is not a regular file: {display}"
328
+ raise ValueError(msg)
329
+ _validate_source_size(path, metadata.st_size)
330
+
331
+
332
+ def _validate_source_size(path: Path, size: int) -> None:
333
+ limit = _MAX_MARKDOWN_FILE_BYTES if path.suffix.lower() in {".md", ".mdx"} else _MAX_SOURCE_FILE_BYTES
334
+ if size > limit:
335
+ msg = f"source input exceeds the {limit // (1024 * 1024)} MiB analysis limit: {path}"
336
+ raise ValueError(msg)
337
+
338
+
339
+ def _is_conventionally_generated(path: Path) -> bool:
340
+ name = path.name.lower()
341
+ return name.endswith((".gen.ts", ".gen.tsx", ".generated.ts", ".generated.tsx")) or name in {
342
+ "next-env.d.ts",
343
+ "worker-configuration.d.ts",
344
+ }
345
+
346
+
347
+ def _is_skill_artifact(path: Path) -> bool:
348
+ parts = path.parts
349
+ return any(root in _SKILL_ARTIFACT_ROOTS and child == "skills" for root, child in pairwise(parts))
350
+
351
+
352
+ def _has_generated_header(path: Path) -> bool:
353
+ try:
354
+ with path.open("rb") as stream:
355
+ lines = stream.read(4096).lower().splitlines()
356
+ except OSError:
357
+ return False
358
+ comments: list[bytes] = []
359
+ in_block = False
360
+ for raw_line in lines[:12]:
361
+ line = raw_line.strip()
362
+ if not line:
363
+ continue
364
+ if in_block:
365
+ comments.append(line)
366
+ in_block = b"*/" not in line and b"-->" not in line
367
+ continue
368
+ if line.startswith((b"#", b"//", b"/*", b"<!--")):
369
+ comments.append(line)
370
+ in_block = (line.startswith(b"/*") and b"*/" not in line) or (
371
+ line.startswith(b"<!--") and b"-->" not in line
372
+ )
373
+ continue
374
+ break
375
+ header = b"\n".join(comments)
376
+ generated = b"auto-generated" in header or b"autogenerated" in header or b"@generated" in header
377
+ hands_off = b"do not edit" in header or b"do not hand-edit" in header or b"do not hand edit" in header
378
+ return generated and hands_off
379
+
380
+
381
+ def _owns_path(path: Path) -> bool:
382
+ return (
383
+ path.name.casefold().endswith(_TERRAFORM_TEST_SUFFIXES)
384
+ or path.suffix.lower() in _SUFFIX_TO_TOOL
385
+ or textlint.is_text_path(path)
386
+ )
387
+
388
+
389
+ def _route_unique_path(grouped: GroupedPaths, path: Path, raw_path: str, seen: set[Path]) -> None:
390
+ key = _path_key(path)
391
+ if key in seen:
392
+ return
393
+ seen.add(key)
394
+ _route_path(grouped, path, raw_path)
395
+
396
+
397
+ def _path_key(path: Path) -> Path:
398
+ # resolve() performs a filesystem lookup for every discovered file.
399
+ return Path(os.path.abspath(path)) # ruff: ignore[os-path-abspath]
400
+
401
+
402
+ def _route_path(grouped: GroupedPaths, path: Path, raw_path: str) -> None:
403
+ tool = (
404
+ _Tool.IAC
405
+ if path.name.casefold().endswith(_TERRAFORM_TEST_SUFFIXES)
406
+ else _SUFFIX_TO_TOOL.get(path.suffix.lower())
407
+ )
408
+ _append_path(grouped, tool, raw_path)
409
+ if textlint.is_text_path(path):
410
+ grouped.text.append(raw_path)
411
+
412
+
413
+ def _append_path(grouped: GroupedPaths, tool: _Tool | None, path: str) -> None:
414
+ match tool:
415
+ case _Tool.PYTHON:
416
+ grouped.python.append(path)
417
+ case _Tool.SQL:
418
+ grouped.sql.append(path)
419
+ case _Tool.IAC:
420
+ grouped.iac.append(path)
421
+ case _Tool.TEXT:
422
+ grouped.text.append(path)
423
+ case _Tool.TYPESCRIPT:
424
+ grouped.typescript.append(path)
425
+ case None:
426
+ return
427
+
428
+
429
+ def _run(
430
+ checker: Callable[[list[str]], int],
431
+ registry: Mapping[str, type[_Rule]],
432
+ files: Sequence[str],
433
+ *,
434
+ extra_args: Sequence[str] = (),
435
+ ) -> int:
436
+ if not files or not registry:
437
+ return 0
438
+ return checker(["check", *_rule_args(registry), *extra_args, "--", *files])
439
+
440
+
441
+ def _rule_args(registry: Mapping[str, type[_Rule]]) -> list[str]:
442
+ return [part for rule_id in sorted(registry) for part in ("--rule", rule_id)]