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,550 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from dataclasses import field as dataclass_field
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ import tomllib
10
+ from typing import TYPE_CHECKING, Final, Literal
11
+
12
+ from packaging.version import InvalidVersion, Version
13
+
14
+ from sarj_standards._meta import CONFIGS_DIR, __version__
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Mapping
19
+
20
+
21
+ MANIFEST_NAME: Final = ".sarj-standards.toml"
22
+ MANIFEST_SCHEMA: Final = 3
23
+ Profile = Literal["standard", "application"]
24
+ PROFILES: Final[tuple[Profile, ...]] = ("standard", "application")
25
+ HookManager = Literal["pre-commit", "lefthook", "none"]
26
+ HOOK_MANAGERS: Final[tuple[HookManager, ...]] = ("pre-commit", "lefthook", "none")
27
+
28
+ PEERS_JSON: Final = CONFIGS_DIR / "eslint.peers.json"
29
+ _ESLINT_RULE_KEY: Final = re.compile(
30
+ r'^\s+"(?P<rule>[^"]+)":\s*(?:"(?:off|warn|error)"|\[)',
31
+ re.MULTILINE,
32
+ )
33
+ _SARJ_RULE_ENGINES: Final = frozenset({"python", "sql", "iac", "text"})
34
+
35
+ #: Sibling distributions pinned exactly by `code-standards`.
36
+ LINT_CONFIGS: Final = "code-standards"
37
+ _PYTHON_LINT: Final = "sarj-python-lint"
38
+ SIBLING_PACKAGES: Final = (_PYTHON_LINT, "sarj-sql-lint", "sarj-iac-lint")
39
+
40
+
41
+ def adopted_version() -> str:
42
+ if __version__ != "0.0.0.dev0":
43
+ return __version__
44
+ source_project = CONFIGS_DIR.parents[2] / "pyproject.toml"
45
+ if not source_project.is_file():
46
+ return __version__
47
+ try:
48
+ parsed: object = tomllib.loads(source_project.read_text(encoding="utf-8"))
49
+ except OSError, tomllib.TOMLDecodeError:
50
+ return __version__
51
+ declared = text_field(table_field(as_table(parsed), "project"), "version")
52
+ return __version__ if declared is None else declared
53
+
54
+
55
+ #: Config bundle selected for each detected ecosystem.
56
+ PYTHON_CONFIGS: Final = ("ruff", "pyright")
57
+ TYPESCRIPT_CONFIGS: Final = ("eslint",)
58
+ SHARED_CONFIGS: Final = ("markdownlint", "taplo", "yamllint")
59
+ ALL_CONFIGS: Final = (*PYTHON_CONFIGS, *TYPESCRIPT_CONFIGS, *SHARED_CONFIGS)
60
+ DEFAULT_DURABLE_ARTIFACTS: Final = (
61
+ "**/README.md",
62
+ "docs/**",
63
+ "**/docs/**",
64
+ "architecture/**",
65
+ "**/adr/**",
66
+ ".github/**",
67
+ "**/AGENTS.md",
68
+ "**/CLAUDE.md",
69
+ )
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ExclusionOverride:
74
+ paths: tuple[str, ...]
75
+ rules: tuple[str, ...]
76
+ reason: str
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class Manifest:
81
+ version: str
82
+ configs: tuple[str, ...]
83
+ python_dest: str
84
+ typescript_dest: str
85
+ profile: Profile = "standard"
86
+ verify_paths: tuple[str, ...] = (".",)
87
+ hook_manager: HookManager = "pre-commit"
88
+ disabled_capabilities: tuple[str, ...] = dataclass_field(default=(), compare=False)
89
+ excluded_paths: tuple[str, ...] = ()
90
+ excluded_rules: tuple[str, ...] = ()
91
+ exclusion_overrides: tuple[ExclusionOverride, ...] = ()
92
+ durable_artifacts: tuple[str, ...] = DEFAULT_DURABLE_ARTIFACTS
93
+ text_excluded_paths: tuple[str, ...] = ()
94
+ doctor_excluded_paths: tuple[str, ...] = ()
95
+ diagnostic_baseline: str | None = None
96
+ ci_bootstrap: tuple[str, ...] = ()
97
+
98
+ def render(self) -> str:
99
+ disabled = tuple(name for name in ALL_CONFIGS if name not in self.configs)
100
+ disabled_text = ", ".join(f'"{name}"' for name in disabled)
101
+ durable_text = ", ".join(json.dumps(value) for value in self.durable_artifacts)
102
+ sections = [
103
+ (
104
+ "# Managed by `code-standards setup`; commit this file.\n"
105
+ f"schema = {MANIFEST_SCHEMA}\n"
106
+ f'bundle = "{self.version}"\n'
107
+ f'profile = "{self.profile}"\n'
108
+ 'rule_profile = "all"\n'
109
+ "\n"
110
+ "[capabilities]\n"
111
+ f"disable = [{disabled_text}]\n"
112
+ "\n"
113
+ "[artifacts]\n"
114
+ f"durable = [{durable_text}]\n"
115
+ "\n"
116
+ "[dest]\n"
117
+ f'python = "{self.python_dest}"\n'
118
+ f'typescript = "{self.typescript_dest}"\n'
119
+ "\n"
120
+ "[hooks]\n"
121
+ f'manager = "{self.hook_manager}"\n'
122
+ )
123
+ ]
124
+ if self.verify_paths != (".",):
125
+ paths = ", ".join(json.dumps(value) for value in self.verify_paths)
126
+ sections.append(f"\n[verify]\npaths = [{paths}]\n")
127
+ if self.excluded_paths or self.excluded_rules:
128
+ paths = ", ".join(json.dumps(value) for value in self.excluded_paths)
129
+ rules = ", ".join(json.dumps(value) for value in self.excluded_rules)
130
+ sections.append(f"\n[exclude]\npaths = [{paths}]\nrules = [{rules}]\n")
131
+ for override in self.exclusion_overrides:
132
+ paths = ", ".join(json.dumps(value) for value in override.paths)
133
+ rules = ", ".join(json.dumps(value) for value in override.rules)
134
+ sections.append(
135
+ f"\n[[exclude.overrides]]\npaths = [{paths}]\nrules = [{rules}]\nreason = {json.dumps(override.reason)}\n"
136
+ )
137
+ if self.text_excluded_paths:
138
+ paths = ", ".join(json.dumps(value) for value in self.text_excluded_paths)
139
+ sections.append(f"\n[text]\nexclude = [{paths}]\n")
140
+ if self.doctor_excluded_paths:
141
+ paths = ", ".join(json.dumps(value) for value in self.doctor_excluded_paths)
142
+ sections.append(f"\n[doctor]\nexclude = [{paths}]\n")
143
+ if self.diagnostic_baseline is not None:
144
+ sections.append(f"\n[baseline]\ndiagnostics = {json.dumps(self.diagnostic_baseline)}\n")
145
+ if self.ci_bootstrap:
146
+ commands = ", ".join(json.dumps(command) for command in self.ci_bootstrap)
147
+ sections.append(f"\n[ci]\nbootstrap = [{commands}]\n")
148
+ return "".join(sections)
149
+
150
+
151
+ def as_table(value: object) -> dict[str, object]:
152
+ if not isinstance(value, dict):
153
+ return {}
154
+ # Centralize untyped-parser narrowing so downstream tables stay typed.
155
+ entries: dict[str, object] = {}
156
+ for key, item in value.items(): # pyright: ignore[reportUnknownVariableType]
157
+ if isinstance(key, str):
158
+ entries[key] = item # ruff: ignore[manual-dict-comprehension] - pyright needs narrowing
159
+ return entries
160
+
161
+
162
+ def text_field(table: Mapping[str, object], key: str) -> str | None:
163
+ value = table.get(key)
164
+ return value if isinstance(value, str) else None
165
+
166
+
167
+ def list_field(table: Mapping[str, object], key: str) -> list[object]:
168
+ value = table.get(key)
169
+ return value if isinstance(value, list) else [] # pyright: ignore[reportUnknownVariableType] — a narrowed `list` from an untyped parser has Unknown leaves
170
+
171
+
172
+ def table_field(table: Mapping[str, object], key: str) -> dict[str, object]:
173
+ return as_table(table.get(key))
174
+
175
+
176
+ def default_configs(*, has_python: bool, has_typescript: bool) -> tuple[str, ...]:
177
+ selected: set[str] = set(SHARED_CONFIGS)
178
+ if has_python:
179
+ selected.update(PYTHON_CONFIGS)
180
+ if has_typescript:
181
+ selected.update(TYPESCRIPT_CONFIGS)
182
+ order = (*PYTHON_CONFIGS, *TYPESCRIPT_CONFIGS, *SHARED_CONFIGS)
183
+ return tuple(name for name in order if name in selected)
184
+
185
+
186
+ def manifest_path(root: Path) -> Path:
187
+ return root / MANIFEST_NAME
188
+
189
+
190
+ def load(root: Path) -> Manifest | None: # ruff: ignore[too-many-locals] - one validation boundary keeps manifest errors coherent.
191
+ path = manifest_path(root)
192
+ if not path.is_file():
193
+ return None
194
+ try:
195
+ parsed: object = tomllib.loads(path.read_text(encoding="utf-8"))
196
+ except tomllib.TOMLDecodeError as exc:
197
+ msg = f"{path} is not valid TOML: {exc}"
198
+ raise ValueError(msg) from exc
199
+
200
+ data = as_table(parsed)
201
+ raw_schema = data.get("schema")
202
+ if type(raw_schema) is not int or raw_schema != MANIFEST_SCHEMA:
203
+ msg = f"{path} `schema` must equal {MANIFEST_SCHEMA}"
204
+ raise ValueError(msg)
205
+ legacy_fields = tuple(field for field in ("version", "configs", "gradual") if field in data)
206
+ if legacy_fields:
207
+ msg = f"{path} uses removed manifest fields: {', '.join(legacy_fields)}"
208
+ raise ValueError(msg)
209
+ declared = text_field(data, "bundle")
210
+ if data.get("rule_profile", "all") != "all":
211
+ msg = f"{path} `rule_profile` currently supports only: all"
212
+ raise ValueError(msg)
213
+ raw_profile = data.get("profile", "standard")
214
+ capabilities_table = _manifest_table(data, "capabilities")
215
+ disabled = _string_list(capabilities_table, "disable", label="manifest [capabilities].disable")
216
+ unknown_capabilities = sorted(set(disabled) - set(ALL_CONFIGS))
217
+ if unknown_capabilities:
218
+ msg = f"manifest disables unknown capabilities: {', '.join(unknown_capabilities)}"
219
+ raise ValueError(msg)
220
+ names = [name for name in ALL_CONFIGS if name not in disabled]
221
+ if declared is None:
222
+ msg = f"{path} must set a string `bundle` declaration"
223
+ raise TypeError(msg)
224
+ try:
225
+ Version(declared)
226
+ except InvalidVersion as exc:
227
+ msg = f"{path} `bundle` must be a valid PEP 440 version"
228
+ raise ValueError(msg) from exc
229
+ if not isinstance(raw_profile, str) or raw_profile not in PROFILES:
230
+ msg = f"{path} `profile` must be one of: {', '.join(PROFILES)}"
231
+ raise ValueError(msg)
232
+ profile: Profile = raw_profile
233
+
234
+ dest_table = _manifest_table(data, "dest")
235
+ verify_table = _manifest_table(data, "verify")
236
+ hooks_table = _manifest_table(data, "hooks")
237
+ exclude_table = _manifest_table(data, "exclude")
238
+ artifacts_table = _manifest_table(data, "artifacts")
239
+ text_table = _manifest_table(data, "text")
240
+ doctor_table = _manifest_table(data, "doctor")
241
+ baseline_table = _manifest_table(data, "baseline")
242
+ ci_table = _manifest_table(data, "ci")
243
+ raw_hook_manager = hooks_table.get("manager", "pre-commit")
244
+ if not isinstance(raw_hook_manager, str) or raw_hook_manager not in HOOK_MANAGERS:
245
+ msg = f"manifest [hooks].manager must be one of: {', '.join(HOOK_MANAGERS)}"
246
+ raise ValueError(msg)
247
+ hook_manager: HookManager = raw_hook_manager
248
+ return Manifest(
249
+ version=declared,
250
+ configs=tuple(names),
251
+ python_dest=_dest_value(dest_table, "python"),
252
+ typescript_dest=_dest_value(dest_table, "typescript"),
253
+ profile=profile,
254
+ verify_paths=_verify_paths(root, verify_table),
255
+ hook_manager=hook_manager,
256
+ disabled_capabilities=disabled,
257
+ excluded_paths=_path_patterns(root, exclude_table, "paths"),
258
+ excluded_rules=_rule_selectors(exclude_table, "rules"),
259
+ exclusion_overrides=_exclusion_overrides(root, exclude_table),
260
+ durable_artifacts=_string_list(
261
+ artifacts_table,
262
+ "durable",
263
+ label="manifest [artifacts].durable",
264
+ default=DEFAULT_DURABLE_ARTIFACTS,
265
+ ),
266
+ text_excluded_paths=_string_list(text_table, "exclude", label="manifest [text].exclude"),
267
+ doctor_excluded_paths=_string_list(doctor_table, "exclude", label="manifest [doctor].exclude"),
268
+ diagnostic_baseline=_relative_file(root, baseline_table, "diagnostics"),
269
+ ci_bootstrap=_ci_bootstrap(ci_table),
270
+ )
271
+
272
+
273
+ def _manifest_table(data: Mapping[str, object], key: str) -> dict[str, object]:
274
+ value = data.get(key)
275
+ if value is None:
276
+ return {}
277
+ if not isinstance(value, dict):
278
+ msg = f"manifest [{key}] must be a table"
279
+ raise TypeError(msg)
280
+ return as_table(value) # pyright: ignore[reportUnknownArgumentType] -- runtime dict narrowed above; as_table validates keys
281
+
282
+
283
+ def load_for_setup(root: Path) -> Manifest | None:
284
+ try:
285
+ return load(root)
286
+ except ValueError:
287
+ legacy = _load_schema_less_manifest(root)
288
+ if legacy is None:
289
+ raise
290
+ return legacy
291
+
292
+
293
+ def _load_schema_less_manifest( # ruff: ignore[too-many-locals] -- validate the complete legacy policy atomically.
294
+ root: Path,
295
+ ) -> Manifest | None:
296
+ path = manifest_path(root)
297
+ try:
298
+ parsed: object = tomllib.loads(path.read_text(encoding="utf-8"))
299
+ except OSError, tomllib.TOMLDecodeError:
300
+ return None
301
+ data = as_table(parsed)
302
+ if "schema" in data or "version" not in data or "configs" not in data:
303
+ return None
304
+ declared = text_field(data, "version")
305
+ raw_configs = list_field(data, "configs")
306
+ if declared is None or not all(isinstance(item, str) and item in ALL_CONFIGS for item in raw_configs):
307
+ return None
308
+ try:
309
+ Version(declared)
310
+ except InvalidVersion:
311
+ return None
312
+ raw_profile = data.get("profile", "standard")
313
+ if not isinstance(raw_profile, str) or raw_profile not in PROFILES:
314
+ return None
315
+ profile: Profile = raw_profile
316
+ dest = table_field(data, "dest")
317
+ try:
318
+ python_dest = _dest_value(dest, "python")
319
+ typescript_dest = _dest_value(dest, "typescript")
320
+ except TypeError:
321
+ return None
322
+ gradual_table = table_field(data, "gradual")
323
+ if "python_baseline" in gradual_table:
324
+ _ = _relative_file(root, gradual_table, "python_baseline")
325
+ msg = (
326
+ "cannot losslessly migrate legacy [gradual].python_baseline to the fingerprint-based "
327
+ "[baseline].diagnostics format; preserve the legacy manifest and replace or retire its baseline "
328
+ "before rerunning setup"
329
+ )
330
+ raise ValueError(msg)
331
+ verify_table = table_field(data, "verify")
332
+ hooks_table = table_field(data, "hooks")
333
+ exclude_table = table_field(data, "exclude")
334
+ raw_hook_manager = hooks_table.get("manager", "pre-commit")
335
+ if not isinstance(raw_hook_manager, str) or raw_hook_manager not in HOOK_MANAGERS:
336
+ msg = f"manifest [hooks].manager must be one of: {', '.join(HOOK_MANAGERS)}"
337
+ raise ValueError(msg)
338
+ hook_manager: HookManager = raw_hook_manager
339
+ return Manifest(
340
+ version=declared,
341
+ configs=tuple(dict.fromkeys(item for item in raw_configs if isinstance(item, str))),
342
+ python_dest=python_dest,
343
+ typescript_dest=typescript_dest,
344
+ profile=profile,
345
+ verify_paths=_verify_paths(root, verify_table),
346
+ hook_manager=hook_manager,
347
+ excluded_paths=_path_patterns(root, exclude_table, "paths"),
348
+ excluded_rules=_rule_selectors(exclude_table, "rules"),
349
+ exclusion_overrides=_exclusion_overrides(root, exclude_table),
350
+ )
351
+
352
+
353
+ def _applicable_configs(root: Path) -> tuple[str, ...]:
354
+ has_python = any((root / name).is_file() for name in ("pyproject.toml", "uv.lock", "poetry.lock", "pdm.lock"))
355
+ has_typescript = any(
356
+ path.is_file()
357
+ for path in (
358
+ root / "package.json",
359
+ root / "package-lock.json",
360
+ root / "pnpm-lock.yaml",
361
+ root / "yarn.lock",
362
+ root / "bun.lock",
363
+ root / "bun.lockb",
364
+ )
365
+ )
366
+ return default_configs(has_python=has_python, has_typescript=has_typescript)
367
+
368
+
369
+ def _string_list(
370
+ table: Mapping[str, object],
371
+ key: str,
372
+ *,
373
+ label: str,
374
+ default: tuple[str, ...] = (),
375
+ ) -> tuple[str, ...]:
376
+ if key not in table:
377
+ return default
378
+ values = list_field(table, key)
379
+ if not all(type(value) is str and value for value in values):
380
+ msg = f"{label} must contain only non-empty strings"
381
+ raise TypeError(msg)
382
+ return tuple(dict.fromkeys(value for value in values if isinstance(value, str)))
383
+
384
+
385
+ def _ci_bootstrap(table: Mapping[str, object]) -> tuple[str, ...]:
386
+ commands = _string_list(table, "bootstrap", label="manifest [ci].bootstrap")
387
+ if any(command != command.strip() or "\n" in command or "\r" in command for command in commands):
388
+ msg = "manifest [ci].bootstrap commands must be trimmed single-line strings"
389
+ raise ValueError(msg)
390
+ return commands
391
+
392
+
393
+ def _path_patterns(root: Path, table: Mapping[str, object], key: str) -> tuple[str, ...]:
394
+ patterns = _string_list(table, key, label=f"manifest [exclude].{key}")
395
+ return tuple(validate_excluded_path(root, pattern) for pattern in patterns)
396
+
397
+
398
+ def _rule_selectors(table: Mapping[str, object], key: str) -> tuple[str, ...]:
399
+ selectors = _string_list(table, key, label=f"manifest [exclude].{key}")
400
+ return tuple(validate_excluded_rule(selector) for selector in selectors)
401
+
402
+
403
+ def validate_excluded_path(root: Path, pattern: str) -> str:
404
+ normalized = pattern.replace("\\", "/")
405
+ if normalized.startswith(("/", "!")) or ".." in normalized.split("/"):
406
+ msg = f"manifest exclusion pattern must be a repository-relative denylist pattern: {pattern}"
407
+ raise ValueError(msg)
408
+ if normalized in {".", "*", "**", "**/*"}:
409
+ msg = f"manifest exclusion pattern is too broad: {pattern}"
410
+ raise ValueError(msg)
411
+ if normalized in {MANIFEST_NAME, f"**/{MANIFEST_NAME}"}:
412
+ msg = "manifest exclusion cannot hide the Standards manifest"
413
+ raise ValueError(msg)
414
+ _ = root
415
+ return normalized
416
+
417
+
418
+ def validate_excluded_rule(selector: str) -> str:
419
+ engine, separator, rule = selector.partition(":")
420
+ if (
421
+ not separator
422
+ or engine not in {"ruff", "basedpyright", "eslint", "python", "sql", "iac", "text"}
423
+ or not rule
424
+ or rule != rule.strip()
425
+ ):
426
+ msg = f"manifest rule exclusion must use a canonical engine:rule selector: {selector}"
427
+ raise ValueError(msg)
428
+ _validate_known_rule(engine, rule, selector)
429
+ return selector
430
+
431
+
432
+ def _validate_known_rule(engine: str, rule: str, selector: str) -> None:
433
+ from sarj_standards.libs.linting import ( # ruff: ignore[import-outside-top-level] -- avoid a manifest/policy import cycle.
434
+ library_policy,
435
+ )
436
+ from sarj_standards.libs.repository import ( # ruff: ignore[import-outside-top-level] -- avoid a manifest/ledger import cycle.
437
+ ledger,
438
+ )
439
+
440
+ shipped = ledger.load()
441
+ if engine in _SARJ_RULE_ENGINES:
442
+ known = frozenset((*shipped.rules.get(engine, ()), *shipped.codes.get(engine, ())))
443
+ if engine == "python":
444
+ known |= frozenset(item.id for item in library_policy.CATALOG)
445
+ if rule not in known:
446
+ msg = f"unknown Standards rule exclusion: {selector}"
447
+ raise ValueError(msg)
448
+ return
449
+ if engine != "eslint":
450
+ return
451
+ if rule.startswith("@sarj/"):
452
+ known = frozenset(f"@sarj/{name}" for name in shipped.rules.get(ledger.ESLINT, ()))
453
+ elif "/" not in rule:
454
+ config = (CONFIGS_DIR / "eslint.strict.mjs").read_text(encoding="utf-8")
455
+ known = frozenset(match.group("rule") for match in _ESLINT_RULE_KEY.finditer(config))
456
+ else:
457
+ return
458
+ if rule not in known:
459
+ msg = f"unknown Standards rule exclusion: {selector}"
460
+ raise ValueError(msg)
461
+
462
+
463
+ def _exclusion_overrides(root: Path, table: Mapping[str, object]) -> tuple[ExclusionOverride, ...]:
464
+ values = list_field(table, "overrides")
465
+ overrides: list[ExclusionOverride] = []
466
+ for value in values:
467
+ item = as_table(value)
468
+ paths = _path_patterns(root, item, "paths")
469
+ rules = _rule_selectors(item, "rules")
470
+ reason = text_field(item, "reason")
471
+ if not paths or not rules or reason is None or not reason.strip():
472
+ msg = "each [[exclude.overrides]] entry must set non-empty paths, rules, and reason"
473
+ raise ValueError(msg)
474
+ overrides.append(ExclusionOverride(paths, rules, reason))
475
+ return tuple(overrides)
476
+
477
+
478
+ def _dest_value(table: dict[str, object], key: str) -> str:
479
+ if key not in table:
480
+ return "."
481
+ value = table[key]
482
+ if not isinstance(value, str) or not value:
483
+ msg = f"manifest [dest].{key} must be a non-empty string"
484
+ raise TypeError(msg)
485
+ return value
486
+
487
+
488
+ def _verify_paths(root: Path, table: dict[str, object]) -> tuple[str, ...]:
489
+ if "paths" not in table:
490
+ return (".",)
491
+ values = list_field(table, "paths")
492
+ if not values or not all(isinstance(value, str) and value for value in values):
493
+ msg = "manifest [verify].paths must be a non-empty list of non-empty strings"
494
+ raise TypeError(msg)
495
+ paths = tuple(value for value in values if isinstance(value, str))
496
+ for value in paths:
497
+ try:
498
+ (root / value).resolve().relative_to(root.resolve())
499
+ except ValueError as exc:
500
+ msg = f"manifest [verify].paths entry escapes repository root: {value}"
501
+ raise ValueError(msg) from exc
502
+ return paths
503
+
504
+
505
+ def _relative_file(root: Path, table: Mapping[str, object], key: str) -> str | None:
506
+ if key not in table:
507
+ return None
508
+ value = text_field(table, key)
509
+ if value is None or not value.strip():
510
+ msg = f"manifest [baseline].{key} must be a non-empty repository-relative path"
511
+ raise TypeError(msg)
512
+ normalized = value.replace("\\", "/")
513
+ path = Path(normalized)
514
+ if path.is_absolute() or ".." in path.parts or path.suffix != ".json":
515
+ msg = f"manifest [baseline].{key} must be a repository-relative JSON path: {value}"
516
+ raise ValueError(msg)
517
+ try:
518
+ (root / path).resolve().relative_to(root.resolve())
519
+ except ValueError as exc:
520
+ msg = f"manifest [baseline].{key} escapes repository root: {value}"
521
+ raise ValueError(msg) from exc
522
+ return path.as_posix()
523
+
524
+
525
+ def installed_versions() -> dict[str, str]:
526
+ found = {LINT_CONFIGS: adopted_version()}
527
+ for name in SIBLING_PACKAGES:
528
+ try:
529
+ found[name] = version(name)
530
+ except PackageNotFoundError:
531
+ continue
532
+ return found
533
+
534
+
535
+ def eslint_peers() -> dict[str, str]:
536
+ parsed: object = json.loads( # pyright: ignore[reportAny] — json.loads is an untyped stdlib boundary; the shape is narrowed below
537
+ PEERS_JSON.read_text(encoding="utf-8")
538
+ )
539
+ table = table_field(as_table(parsed), "peers")
540
+ if not table:
541
+ msg = f"{PEERS_JSON} must contain a `peers` object"
542
+ raise TypeError(msg)
543
+ return {name: pin for name, pin in table.items() if isinstance(pin, str)}
544
+
545
+
546
+ def eslint_overrides() -> dict[str, object]:
547
+ parsed: object = json.loads( # pyright: ignore[reportAny] — json.loads is an untyped stdlib boundary; the shape is narrowed below
548
+ PEERS_JSON.read_text(encoding="utf-8")
549
+ )
550
+ return table_field(as_table(parsed), "npmOverrides")