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,744 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping, Sequence
4
+ from dataclasses import dataclass
5
+ from fnmatch import fnmatch
6
+ import hashlib
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import re
11
+ import shutil
12
+ import subprocess # ruff: ignore[suspicious-subprocess-import] -- fixed-argument git queries are required repository inputs.
13
+ import tomllib
14
+ from typing import Final
15
+
16
+ import yaml
17
+
18
+ from sarj_standards.libs.adoption.manifest import as_table, list_field, table_field, text_field
19
+
20
+
21
+ _CONFLICT_RE: Final = re.compile(r"^(?:<<<<<<< |>>>>>>> |\|\|\|\|\|\|\| )", re.MULTILINE)
22
+ _GITHUB_MERGE_SUBJECT_RE: Final = re.compile(r"^Merge pull request #[1-9][0-9]* from [A-Za-z0-9_.-]+/[A-Za-z0-9._/-]+$")
23
+ _PRIVATE_REFS_FILE: Final = ".sarj-private-refs.toml"
24
+ _TEST_COMMAND_RE: Final = re.compile(r"(?:npm test|pytest|make (?:verify|test)\b)")
25
+ _PYPROJECT_VERSION_RE: Final = re.compile(r'^version = "([^"]+)"$', re.MULTILINE)
26
+ _ESLINT_RULE_RE: Final = re.compile(r'^\s*"([a-z0-9-]+)":', re.MULTILINE)
27
+ _ESLINT_MAP_RE: Final = re.compile(r"^const rules = \{$(?P<body>.*?)^\};$", re.MULTILINE | re.DOTALL)
28
+ _MARKDOWN_LOCATIONS: Final = (
29
+ ".github/SECURITY.md",
30
+ ".github/PULL_REQUEST_TEMPLATE.md",
31
+ "CODE_OF_CONDUCT.md",
32
+ "CONTRIBUTING.md",
33
+ "README.md",
34
+ "CLAUDE.md",
35
+ "packages/*/README.md",
36
+ "plugins/*/commands/*.md",
37
+ "plugins/*/skills/*/SKILL.md",
38
+ "plugins/*/skills/*/references/*.md",
39
+ "plugins/*/README.md",
40
+ "docs/audits/*.md",
41
+ )
42
+ _MANAGED_ROOT_CONFIGS: Final = (
43
+ (".ruff-strict.toml", "ruff.strict.toml"),
44
+ (".pyright-strict.json", "pyright.strict.json"),
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class Finding:
50
+ check: str
51
+ where: str
52
+ message: str
53
+
54
+ def render(self) -> str:
55
+ return f"error[{self.check}]: {self.where}: {self.message}"
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class FilenameRule:
60
+ glob: str
61
+ pattern: re.Pattern[str]
62
+ label: str
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class RuleFamily:
67
+ name: str
68
+ source: str
69
+ tests: str
70
+ registry: str
71
+ extension: str
72
+ test_pattern: str
73
+ registry_pattern: str
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class ConfigReference:
78
+ glob: str
79
+ pattern: re.Pattern[str]
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class VersionReference:
84
+ path: str
85
+ format: str
86
+ version: str
87
+ selector: str
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class RepositoryPolicy:
92
+ distinctive: tuple[str, ...]
93
+ contextual: tuple[str, ...]
94
+ private_excludes: tuple[str, ...]
95
+ forbidden_paths: tuple[str, ...]
96
+ filename_rules: tuple[FilenameRule, ...]
97
+ rule_families: tuple[RuleFamily, ...]
98
+ config_references: tuple[ConfigReference, ...]
99
+ version_references: tuple[VersionReference, ...]
100
+ canonical_config_dir: str
101
+ versions: Mapping[str, tuple[str, ...]]
102
+ known_manifests: tuple[str, ...]
103
+ known_locks: tuple[str, ...]
104
+
105
+
106
+ def load_policy(root: Path, *, private_refs_path: Path | None = None) -> RepositoryPolicy:
107
+ path = root / ".sarj-standards.toml"
108
+ try:
109
+ raw: object = tomllib.loads(path.read_text(encoding="utf-8"))
110
+ except (OSError, tomllib.TOMLDecodeError) as exc:
111
+ msg = f"cannot load repository policy from {path}: {exc}"
112
+ raise ValueError(msg) from exc
113
+ repository = _table(as_table(raw), "repository", required=True)
114
+ _known_keys(
115
+ repository,
116
+ frozenset(
117
+ {
118
+ "canonical_config_dir",
119
+ "config_references",
120
+ "filename_rules",
121
+ "forbidden_paths",
122
+ "private_refs",
123
+ "rule_families",
124
+ "version_coverage",
125
+ "version_references",
126
+ "versions",
127
+ }
128
+ ),
129
+ "repository",
130
+ )
131
+ private = _private_refs(root, _table(repository, "private_refs"), private_refs_path)
132
+ _known_keys(
133
+ private,
134
+ frozenset({"contextual", "distinctive", "exclude"}),
135
+ "private_refs",
136
+ )
137
+ coverage = _table(repository, "version_coverage")
138
+ _known_keys(coverage, frozenset({"locks", "manifests"}), "version_coverage")
139
+ return RepositoryPolicy(
140
+ distinctive=_strings(private, "distinctive"),
141
+ contextual=_strings(private, "contextual"),
142
+ private_excludes=_strings(private, "exclude"),
143
+ forbidden_paths=_strings(repository, "forbidden_paths"),
144
+ filename_rules=tuple(_filename_rules(_objects(repository, "filename_rules"))),
145
+ rule_families=tuple(_rule_families(_objects(repository, "rule_families"))),
146
+ config_references=tuple(_config_references(_objects(repository, "config_references"))),
147
+ version_references=tuple(_version_references(_objects(repository, "version_references"))),
148
+ canonical_config_dir=_optional_text(repository, "canonical_config_dir"),
149
+ versions={
150
+ key: _string_values(value, f"repository.versions.{key}")
151
+ for key, value in _table(repository, "versions").items()
152
+ },
153
+ known_manifests=_strings(coverage, "manifests"),
154
+ known_locks=_strings(coverage, "locks"),
155
+ )
156
+
157
+
158
+ def check(
159
+ root: Path,
160
+ *,
161
+ selected: frozenset[str] = frozenset(),
162
+ commits: str | None = None,
163
+ policy_root: Path | None = None,
164
+ private_refs_path: Path | None = None,
165
+ ) -> list[Finding]:
166
+ if policy_root is not None and policy_root.resolve() != root.resolve() and selected != frozenset({"private-refs"}):
167
+ msg = "a separate policy root is restricted to the private-refs check"
168
+ raise ValueError(msg)
169
+ policy = load_policy(policy_root or root, private_refs_path=private_refs_path)
170
+ checks = {
171
+ "ci-history": lambda: check_ci_history(root),
172
+ "file-conventions": lambda: check_file_conventions(root, policy),
173
+ "private-refs": lambda: check_private_refs(root, policy, commits=commits),
174
+ "versions": lambda: check_versions(root, policy),
175
+ }
176
+ unknown = selected.difference(checks)
177
+ if unknown:
178
+ msg = f"unknown repository check(s): {', '.join(sorted(unknown))}"
179
+ raise ValueError(msg)
180
+ findings: list[Finding] = []
181
+ defaults = frozenset(checks).difference({"private-refs"})
182
+ for name, checker in checks.items():
183
+ if name in (selected or defaults):
184
+ findings.extend(checker())
185
+ return sorted(findings, key=lambda item: (item.check, item.where, item.message))
186
+
187
+
188
+ def check_private_refs(root: Path, policy: RepositoryPolicy, *, commits: str | None) -> list[Finding]:
189
+ if not any((policy.distinctive, policy.contextual)):
190
+ msg = "private-reference policy is unavailable"
191
+ raise ValueError(msg)
192
+ broad = _broad_private_pattern(policy.distinctive)
193
+ scoped = _scoped_private_pattern(policy.contextual)
194
+ findings: list[Finding] = []
195
+ for relative in _tracked(root):
196
+ findings.extend(_private_text_findings(relative, relative, broad, scoped))
197
+ if any(fnmatch(relative, pattern) for pattern in policy.private_excludes):
198
+ continue
199
+ text = _tracked_text(root, relative)
200
+ findings.extend(_private_text_findings(relative, text, broad, scoped))
201
+ if commits:
202
+ revisions = _git(root, "rev-list", "--reverse", commits, check=False)
203
+ if revisions.returncode != 0:
204
+ findings.append(Finding("private-refs", commits, "commit range does not resolve; fetch full history"))
205
+ else:
206
+ findings.extend(_commit_findings(root, revisions.stdout.splitlines(), policy, broad, scoped))
207
+ return list(dict.fromkeys(findings))
208
+
209
+
210
+ def _commit_findings(
211
+ root: Path,
212
+ revisions: Sequence[str],
213
+ policy: RepositoryPolicy,
214
+ broad: re.Pattern[str] | None,
215
+ scoped: re.Pattern[str] | None,
216
+ ) -> list[Finding]:
217
+ findings: list[Finding] = []
218
+ for revision in revisions:
219
+ message = _git(root, "show", "--quiet", "--format=%H%n%s%n%b", revision).stdout
220
+ parents = _git(root, "show", "--quiet", "--format=%P", revision).stdout.split()
221
+ if len(parents) > 1:
222
+ message = _without_generated_merge_subject(message)
223
+ if _matches_private_ref(message, broad, scoped) or _CONFLICT_RE.search(message):
224
+ findings.append(Finding("private-refs", revision, "private reference or conflict marker in commit message"))
225
+ changed = _git(
226
+ root,
227
+ "diff-tree",
228
+ "--root",
229
+ "-m",
230
+ "-r",
231
+ "--no-commit-id",
232
+ "--no-renames",
233
+ "--name-only",
234
+ "-z",
235
+ revision,
236
+ ).stdout
237
+ for relative in (path for path in changed.split("\0") if path):
238
+ where = f"{revision}:{relative}"
239
+ findings.extend(_private_text_findings(where, relative, broad, scoped))
240
+ if any(fnmatch(relative, pattern) for pattern in policy.private_excludes):
241
+ continue
242
+ text = _revision_text(root, revision, relative)
243
+ if text is None:
244
+ continue
245
+ findings.extend(_private_text_findings(where, text, broad, scoped))
246
+ return findings
247
+
248
+
249
+ def _without_generated_merge_subject(message: str) -> str:
250
+ commit_hash, separator, remainder = message.partition("\n")
251
+ subject, body_separator, body = remainder.partition("\n")
252
+ if separator and _GITHUB_MERGE_SUBJECT_RE.fullmatch(subject):
253
+ return f"{commit_hash}\n{body}" if body_separator else commit_hash
254
+ return message
255
+
256
+
257
+ def _private_text_findings(
258
+ where: str,
259
+ text: str,
260
+ broad: re.Pattern[str] | None,
261
+ scoped: re.Pattern[str] | None,
262
+ ) -> list[Finding]:
263
+ findings: list[Finding] = []
264
+ if _matches_private_ref(text, broad, scoped):
265
+ findings.append(Finding("private-refs", where, "private repository or client reference"))
266
+ if _CONFLICT_RE.search(text):
267
+ findings.append(Finding("private-refs", where, "unresolved conflict marker"))
268
+ return findings
269
+
270
+
271
+ def _revision_text(root: Path, revision: str, relative: str) -> str | None:
272
+ entry = _git(root, "ls-tree", "-z", revision, "--", relative).stdout.rstrip("\0")
273
+ if not entry:
274
+ return None
275
+ metadata, separator, _ = entry.partition("\t")
276
+ if not separator:
277
+ return None
278
+ match metadata.split():
279
+ case [_, "blob", object_id]:
280
+ pass
281
+ case _:
282
+ return None
283
+ text = _git(root, "cat-file", "blob", object_id).stdout
284
+ return "" if "\0" in text else text
285
+
286
+
287
+ def _private_refs(
288
+ root: Path,
289
+ public: Mapping[str, object],
290
+ private_refs_path: Path | None,
291
+ ) -> Mapping[str, object]:
292
+ local_path = private_refs_path or root / _PRIVATE_REFS_FILE
293
+ if local_path.is_file():
294
+ try:
295
+ parsed: object = tomllib.loads(local_path.read_text(encoding="utf-8"))
296
+ except (OSError, tomllib.TOMLDecodeError) as exc:
297
+ msg = f"cannot load private-reference policy from {local_path}: {exc}"
298
+ raise ValueError(msg) from exc
299
+ return {**public, **_table(as_table(parsed), "private_refs", required=True)}
300
+ return public
301
+
302
+
303
+ def _broad_private_pattern(literals: Sequence[str]) -> re.Pattern[str] | None:
304
+ if not literals:
305
+ return None
306
+ alternatives = _private_alternation(literals)
307
+ return re.compile(rf"(^|[^A-Za-z0-9])(?:{alternatives})(?:[^A-Za-z0-9]|$)", re.IGNORECASE)
308
+
309
+
310
+ def _scoped_private_pattern(literals: Sequence[str]) -> re.Pattern[str] | None:
311
+ if not literals:
312
+ return None
313
+ alternatives = _private_alternation(literals)
314
+ return re.compile(
315
+ rf"(^|[^A-Za-z0-9])(?:{alternatives})(?:/[A-Za-z0-9_.]|'s[^A-Za-z0-9]|'\s)|"
316
+ rf"^\s*\|\s*(?:{alternatives})\s*\|",
317
+ re.IGNORECASE | re.MULTILINE,
318
+ )
319
+
320
+
321
+ def _matches_private_ref(
322
+ text: str,
323
+ broad: re.Pattern[str] | None,
324
+ scoped: re.Pattern[str] | None,
325
+ ) -> bool:
326
+ return bool((broad and broad.search(text)) or (scoped and scoped.search(text)))
327
+
328
+
329
+ def check_ci_history(root: Path) -> list[Finding]:
330
+ findings: list[Finding] = []
331
+ for path in sorted((root / ".github/workflows").glob("*.yml")):
332
+ try:
333
+ document: object = yaml.safe_load(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
334
+ except (OSError, yaml.YAMLError) as exc:
335
+ findings.append(Finding("ci-history", str(path.relative_to(root)), f"invalid workflow YAML: {exc}"))
336
+ continue
337
+ jobs = table_field(as_table(document), "jobs")
338
+ for job_name, raw_job in jobs.items():
339
+ job = as_table(raw_job)
340
+ steps = list_field(job, "steps")
341
+ runs_tests = any(_TEST_COMMAND_RE.search(str(as_table(step).get("run", ""))) for step in steps)
342
+ if not runs_tests:
343
+ continue
344
+ full_history = any(_is_full_checkout(as_table(step)) for step in steps)
345
+ if not full_history:
346
+ relative = path.relative_to(root)
347
+ findings.append(
348
+ Finding("ci-history", f"{relative}:{job_name}", "test job needs checkout fetch-depth: 0")
349
+ )
350
+ return findings
351
+
352
+
353
+ def check_file_conventions(root: Path, policy: RepositoryPolicy) -> list[Finding]:
354
+ tracked = _tracked(root)
355
+ findings = [
356
+ Finding("file-conventions", relative, "path is forbidden by repository policy")
357
+ for relative in tracked
358
+ if any(fnmatch(relative, pattern) for pattern in policy.forbidden_paths)
359
+ ]
360
+ for rule in policy.filename_rules:
361
+ findings.extend(
362
+ Finding("file-conventions", relative, rule.label)
363
+ for relative in tracked
364
+ if fnmatch(relative, rule.glob) and not rule.pattern.fullmatch(Path(relative).name)
365
+ )
366
+ findings.extend(_check_markdown_locations(tracked))
367
+ for family in policy.rule_families:
368
+ findings.extend(_check_rule_family(root, family))
369
+ findings.extend(_check_config_copies(root, tracked, policy.canonical_config_dir))
370
+ findings.extend(_check_config_references(root, tracked, policy))
371
+ return findings
372
+
373
+
374
+ def check_versions(root: Path, policy: RepositoryPolicy) -> list[Finding]:
375
+ versions = {name: _manifest_version(root / paths[0]) for name, paths in policy.versions.items() if paths}
376
+ findings: list[Finding] = []
377
+ for name, paths in policy.versions.items():
378
+ expected = versions.get(name)
379
+ for relative in paths:
380
+ actual = _manifest_version(root / relative)
381
+ if expected is None or actual is None:
382
+ findings.append(Finding("versions", relative, f"cannot read {name} version"))
383
+ elif actual != expected:
384
+ findings.append(Finding("versions", relative, f"version {actual} does not match {expected}"))
385
+ for reference in policy.version_references:
386
+ expected = versions.get(reference.version)
387
+ actual = _reference_version(root / reference.path, reference)
388
+ if expected != actual:
389
+ findings.append(
390
+ Finding(
391
+ "versions",
392
+ reference.path,
393
+ f"{reference.selector} is {actual or 'missing'}, expected {expected}",
394
+ )
395
+ )
396
+ findings.extend(_check_version_coverage(root, policy))
397
+ return findings
398
+
399
+
400
+ def _reference_version(path: Path, reference: VersionReference) -> str | None:
401
+ if reference.format == "uv-lock":
402
+ return _uv_lock_version(path, reference.selector)
403
+ if reference.format == "exact-pin":
404
+ match = re.search(rf'"{re.escape(reference.selector)}==([^"]+)"', _read_text(path))
405
+ return match.group(1) if match else None
406
+ if reference.format != "json-pointer":
407
+ msg = f"unknown version reference format: {reference.format}"
408
+ raise ValueError(msg)
409
+ try:
410
+ document: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
411
+ except OSError, json.JSONDecodeError:
412
+ return None
413
+ value = document
414
+ for token in reference.selector.removeprefix("/").split("/"):
415
+ value = as_table(value).get(token.replace("~1", "/").replace("~0", "~"))
416
+ return value if isinstance(value, str) else None
417
+
418
+
419
+ def _check_markdown_locations(tracked: Sequence[str]) -> list[Finding]:
420
+ return [
421
+ Finding("file-conventions", path, "Markdown is outside the maintained locations")
422
+ for path in tracked
423
+ if path.endswith(".md") and not any(fnmatch(path, pattern) for pattern in _MARKDOWN_LOCATIONS)
424
+ ]
425
+
426
+
427
+ def _check_rule_family(root: Path, family: RuleFamily) -> list[Finding]:
428
+ source = root / family.source
429
+ tests = root / family.tests
430
+ registry = _read_text(root / family.registry)
431
+ findings: list[Finding] = []
432
+ for path in sorted(source.glob(f"*.{family.extension}")):
433
+ if path.stem.startswith("_"):
434
+ continue
435
+ test = tests / family.test_pattern.format(name=path.stem)
436
+ if not test.is_file():
437
+ findings.append(
438
+ Finding("file-conventions", str(path.relative_to(root)), f"missing {test.relative_to(root)}")
439
+ )
440
+ if family.registry_pattern.format(name=path.stem) not in registry:
441
+ findings.append(
442
+ Finding("file-conventions", str(path.relative_to(root)), "rule is absent from its registry")
443
+ )
444
+ test_glob = family.test_pattern.format(name="*")
445
+ prefix, suffix = family.test_pattern.split("{name}", maxsplit=1)
446
+ for path in sorted(tests.glob(test_glob)):
447
+ name = path.name.removeprefix(prefix).removesuffix(suffix)
448
+ if (
449
+ not (source / f"{name}.{family.extension}").is_file()
450
+ and not (source / f"_{name}.{family.extension}").is_file()
451
+ ):
452
+ findings.append(Finding("file-conventions", str(path.relative_to(root)), "test names no rule or helper"))
453
+ return findings
454
+
455
+
456
+ def _check_config_copies(root: Path, tracked: Sequence[str], canonical_dir: str) -> list[Finding]:
457
+ canonical_root = root / canonical_dir
458
+ hashes = {_digest(path): path for path in canonical_root.iterdir() if path.is_file()}
459
+ findings: list[Finding] = []
460
+ for relative in tracked:
461
+ path = root / relative
462
+ if path.is_symlink() or not path.is_file() or canonical_root in path.parents:
463
+ continue
464
+ managed_source = _managed_config_source(root, path, canonical_root)
465
+ if managed_source is not None:
466
+ if _digest(path) != _digest(managed_source):
467
+ findings.append(
468
+ Finding(
469
+ "file-conventions",
470
+ relative,
471
+ f"generated config drifted from {managed_source.relative_to(root)}; run `code-standards setup`",
472
+ )
473
+ )
474
+ continue
475
+ original = hashes.get(_digest(path))
476
+ if original is not None:
477
+ findings.append(
478
+ Finding(
479
+ "file-conventions",
480
+ relative,
481
+ f"duplicates {original.relative_to(root)}; remove the unmanaged copy",
482
+ )
483
+ )
484
+ return findings
485
+
486
+
487
+ def _managed_config_source(root: Path, path: Path, canonical_root: Path) -> Path | None:
488
+ for destination, source in _MANAGED_ROOT_CONFIGS:
489
+ candidate = canonical_root / source
490
+ if path == root / destination and candidate.is_file():
491
+ return candidate
492
+ return None
493
+
494
+
495
+ def _is_full_checkout(step: Mapping[str, object]) -> bool:
496
+ if step.get("uses") is None or not str(step["uses"]).startswith("actions/checkout@"):
497
+ return False
498
+ depth = table_field(step, "with").get("fetch-depth")
499
+ return depth in {0, "0"}
500
+
501
+
502
+ def _manifest_version(path: Path) -> str | None:
503
+ if path.suffix == ".json":
504
+ try:
505
+ value: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
506
+ except OSError, json.JSONDecodeError:
507
+ return None
508
+ return text_field(as_table(value), "version")
509
+ match = _PYPROJECT_VERSION_RE.search(_read_text(path)[:2000])
510
+ return match.group(1) if match else None
511
+
512
+
513
+ def _uv_lock_version(path: Path, distribution: str) -> str | None:
514
+ try:
515
+ document: object = tomllib.loads(path.read_text(encoding="utf-8"))
516
+ except OSError, tomllib.TOMLDecodeError:
517
+ return None
518
+ packages = list_field(as_table(document), "package")
519
+ for raw in packages:
520
+ package = as_table(raw)
521
+ if text_field(package, "name") == distribution:
522
+ return text_field(package, "version")
523
+ return None
524
+
525
+
526
+ def _tracked(root: Path) -> tuple[str, ...]:
527
+ output = _git(root, "ls-files", "-z").stdout
528
+ return tuple(path for path in output.split("\0") if path)
529
+
530
+
531
+ def _git(root: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
532
+ executable = shutil.which("git")
533
+ if executable is None:
534
+ msg = "git is required for repository checks"
535
+ raise OSError(msg)
536
+ environment = os.environ.copy() # ruff: ignore[banned-api] -- preserve user Git configuration, but not a hook's repository binding.
537
+ local_names = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
538
+ [executable, "rev-parse", "--local-env-vars"],
539
+ check=True,
540
+ capture_output=True,
541
+ text=True,
542
+ ).stdout.splitlines()
543
+ for name in local_names:
544
+ environment.pop(name, None)
545
+ return subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
546
+ [executable, *args],
547
+ cwd=root,
548
+ check=check,
549
+ capture_output=True,
550
+ env=environment,
551
+ errors="replace",
552
+ text=True,
553
+ )
554
+
555
+
556
+ def _read_text(path: Path) -> str:
557
+ content = path.read_bytes() if path.is_file() else b""
558
+ return "" if b"\0" in content else content.decode("utf-8", errors="replace")
559
+
560
+
561
+ def _tracked_text(root: Path, relative: str) -> str:
562
+ path = root / relative
563
+ if path.is_symlink():
564
+ return path.readlink().as_posix()
565
+ resolved_root = root.resolve()
566
+ resolved = path.resolve()
567
+ if resolved != resolved_root and not resolved.is_relative_to(resolved_root):
568
+ msg = f"tracked path escapes repository: {relative}"
569
+ raise ValueError(msg)
570
+ return _read_text(resolved)
571
+
572
+
573
+ def _digest(path: Path) -> str:
574
+ return hashlib.sha256(path.read_bytes()).hexdigest()
575
+
576
+
577
+ def _private_alternation(literals: Sequence[str]) -> str:
578
+ return "|".join(map(re.escape, literals))
579
+
580
+
581
+ def _strings(table: Mapping[str, object], key: str) -> tuple[str, ...]:
582
+ if key not in table:
583
+ return ()
584
+ return _string_values(table[key], key)
585
+
586
+
587
+ def _string_values(value: object, label: str) -> tuple[str, ...]:
588
+ if not isinstance(value, list):
589
+ msg = f"{label} must be a list of strings"
590
+ raise TypeError(msg)
591
+ items: list[object] = value # pyright: ignore[reportUnknownVariableType]
592
+ strings = tuple(item for item in items if isinstance(item, str))
593
+ if len(strings) != len(items):
594
+ msg = f"{label} must contain only strings"
595
+ raise ValueError(msg)
596
+ return strings
597
+
598
+
599
+ def _objects(table: Mapping[str, object], key: str) -> list[object]:
600
+ if key not in table:
601
+ return []
602
+ value = table[key]
603
+ if not isinstance(value, list):
604
+ msg = f"repository.{key} must be an array of tables"
605
+ raise TypeError(msg)
606
+ return value # pyright: ignore[reportUnknownVariableType]
607
+
608
+
609
+ def _table(table: Mapping[str, object], key: str, *, required: bool = False) -> Mapping[str, object]:
610
+ if key not in table:
611
+ if required:
612
+ msg = f"missing [{key}] table"
613
+ raise ValueError(msg)
614
+ return {}
615
+ value = table[key]
616
+ if not isinstance(value, dict):
617
+ msg = f"{key} must be a table"
618
+ raise TypeError(msg)
619
+ return as_table(value) # pyright: ignore[reportUnknownArgumentType]
620
+
621
+
622
+ def _known_keys(table: Mapping[str, object], allowed: frozenset[str], label: str) -> None:
623
+ unknown = set(table).difference(allowed)
624
+ if unknown:
625
+ msg = f"unknown {label} field(s): {', '.join(sorted(unknown))}"
626
+ raise ValueError(msg)
627
+
628
+
629
+ def _optional_text(table: Mapping[str, object], key: str) -> str:
630
+ if key not in table:
631
+ return ""
632
+ value = table[key]
633
+ if not isinstance(value, str):
634
+ msg = f"repository.{key} must be a string"
635
+ raise TypeError(msg)
636
+ return value
637
+
638
+
639
+ def _required_texts(table: Mapping[str, object], keys: tuple[str, ...], label: str) -> tuple[str, ...]:
640
+ values = tuple(text_field(table, key) for key in keys)
641
+ missing = tuple(key for key, value in zip(keys, values, strict=True) if not value)
642
+ if missing:
643
+ msg = f"{label} requires: {', '.join(missing)}"
644
+ raise ValueError(msg)
645
+ return tuple(value or "" for value in values)
646
+
647
+
648
+ def _filename_rules(values: Iterable[object]) -> Iterable[FilenameRule]:
649
+ for value in values:
650
+ table = as_table(value)
651
+ _known_keys(table, frozenset({"glob", "label", "pattern"}), "filename rule")
652
+ glob, pattern, label = _required_texts(table, ("glob", "pattern", "label"), "filename rule")
653
+ yield FilenameRule(glob, _compile_policy_regex(pattern, "filename rule"), label)
654
+
655
+
656
+ def _rule_families(values: Iterable[object]) -> Iterable[RuleFamily]:
657
+ for value in values:
658
+ table = as_table(value)
659
+ _known_keys(
660
+ table,
661
+ frozenset({"extension", "name", "registry", "registry_pattern", "source", "test_pattern", "tests"}),
662
+ "rule family",
663
+ )
664
+ fields = _required_texts(
665
+ table,
666
+ ("name", "source", "tests", "registry", "extension", "test_pattern", "registry_pattern"),
667
+ "rule family",
668
+ )
669
+ name, source, tests, registry, extension, test_pattern, registry_pattern = fields
670
+ yield RuleFamily(name, source, tests, registry, extension, test_pattern, registry_pattern)
671
+
672
+
673
+ def _config_references(values: Iterable[object]) -> Iterable[ConfigReference]:
674
+ for value in values:
675
+ table = as_table(value)
676
+ _known_keys(table, frozenset({"glob", "pattern"}), "config reference")
677
+ glob, pattern = _required_texts(table, ("glob", "pattern"), "config reference")
678
+ yield ConfigReference(glob, _compile_policy_regex(pattern, "config reference", re.MULTILINE))
679
+
680
+
681
+ def _version_references(values: Iterable[object]) -> Iterable[VersionReference]:
682
+ for value in values:
683
+ table = as_table(value)
684
+ _known_keys(table, frozenset({"format", "path", "selector", "version"}), "version reference")
685
+ path, format_name, version, selector = _required_texts(
686
+ table, ("path", "format", "version", "selector"), "version reference"
687
+ )
688
+ yield VersionReference(path, format_name, version, selector)
689
+
690
+
691
+ def _compile_policy_regex(pattern: str, label: str, flags: re.RegexFlag = re.NOFLAG) -> re.Pattern[str]:
692
+ try:
693
+ return re.compile(pattern, flags)
694
+ except re.PatternError as exc:
695
+ msg = f"invalid {label} regex: {pattern}"
696
+ raise ValueError(msg) from exc
697
+
698
+
699
+ def _check_config_references(root: Path, tracked: Sequence[str], policy: RepositoryPolicy) -> list[Finding]:
700
+ canonical = (root / policy.canonical_config_dir).resolve()
701
+ findings: list[Finding] = []
702
+ for rule in policy.config_references:
703
+ for relative in tracked:
704
+ if not fnmatch(relative, rule.glob):
705
+ continue
706
+ match = rule.pattern.search(_read_text(root / relative))
707
+ if match is None:
708
+ continue
709
+ reference = match.group(1)
710
+ target = root / Path(relative).parent / reference
711
+ if not target.exists():
712
+ findings.append(Finding("file-conventions", relative, f"extended config does not exist: {reference}"))
713
+ elif (
714
+ canonical not in target.resolve().parents
715
+ and _managed_config_source(root, target.resolve(), canonical) is None
716
+ ):
717
+ findings.append(
718
+ Finding("file-conventions", relative, f"extended config is outside {policy.canonical_config_dir}")
719
+ )
720
+ return findings
721
+
722
+
723
+ def _check_version_coverage(root: Path, policy: RepositoryPolicy) -> list[Finding]:
724
+ known_manifests = set(policy.known_manifests)
725
+ known_locks = set(policy.known_locks)
726
+ tracked = _tracked(root)
727
+ manifests = {
728
+ path
729
+ for path in tracked
730
+ if fnmatch(path, "packages/*/pyproject.toml") or fnmatch(path, "packages/*/package.json")
731
+ }
732
+ locks = {path for path in tracked if path.endswith(("uv.lock", "package-lock.json"))}
733
+ return [
734
+ Finding("versions", path, "versioned package manifest is absent from version policy")
735
+ for path in sorted(manifests - known_manifests)
736
+ ] + [Finding("versions", path, "lockfile is absent from version policy") for path in sorted(locks - known_locks)]
737
+
738
+
739
+ def eslint_rule_names(root: Path) -> list[str]:
740
+ source = _read_text(root / "packages/typescript/src/index.ts")
741
+ body = _ESLINT_MAP_RE.search(source)
742
+ if body is None:
743
+ return []
744
+ return sorted(_ESLINT_RULE_RE.findall(body.group("body")))