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,382 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ import json
6
+ from pathlib import PurePosixPath
7
+ import re
8
+ from typing import TYPE_CHECKING, Final, NewType, Self
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterable
13
+
14
+
15
+ _DEFAULT_CASE_PATH = PurePosixPath("case.txt")
16
+ _KEBAB_CASE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
17
+ _MAX_SUMMARY_LENGTH: Final = 160
18
+ _PUBLIC_PAIR_SIZE: Final = 2
19
+
20
+ RuleId = NewType("RuleId", str)
21
+ MessageId = NewType("MessageId", str)
22
+
23
+
24
+ class Language(StrEnum):
25
+ PYTHON = "python"
26
+ TYPESCRIPT = "typescript"
27
+ MARKDOWN = "markdown"
28
+ SQL = "sql"
29
+ IAC = "iac"
30
+ CONFIG = "config"
31
+
32
+
33
+ class RuleOrigin(StrEnum):
34
+ UPSTREAM = "upstream"
35
+ SARJ = "sarj"
36
+
37
+
38
+ class RuleEngine(StrEnum):
39
+ ESLINT = "eslint"
40
+ IAC = "iac"
41
+ PYTHON = "python"
42
+ SQL = "sql"
43
+ TEXT = "text"
44
+
45
+
46
+ @dataclass(frozen=True, slots=True, order=True)
47
+ class RuleSelector:
48
+ engine: RuleEngine
49
+ rule_id: RuleId
50
+
51
+ def __post_init__(self) -> None:
52
+ if not _KEBAB_CASE.fullmatch(self.rule_id):
53
+ msg = "rule ID must be non-empty lowercase kebab-case"
54
+ raise ValueError(msg)
55
+
56
+ @classmethod
57
+ def parse(cls, value: str) -> Self:
58
+ engine_text, separator, rule_text = value.partition(":")
59
+ if not separator or ":" in rule_text:
60
+ msg = "rule selector must use canonical ENGINE:ID form"
61
+ raise ValueError(msg)
62
+ try:
63
+ engine = RuleEngine(engine_text)
64
+ except ValueError as exc:
65
+ msg = f"unknown custom-rule engine: {engine_text}"
66
+ raise ValueError(msg) from exc
67
+ return cls(engine, RuleId(rule_text))
68
+
69
+ def __str__(self) -> str:
70
+ return f"{self.engine.value}:{self.rule_id}"
71
+
72
+ @property
73
+ def native_rule_id(self) -> str:
74
+ if self.engine is RuleEngine.ESLINT:
75
+ return f"@sarj/{self.rule_id}"
76
+ return str(self.rule_id)
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class RuleSelection:
81
+ selectors: frozenset[RuleSelector]
82
+
83
+ @classmethod
84
+ def from_values(cls, values: Iterable[str | RuleSelector]) -> Self:
85
+ if isinstance(values, str):
86
+ msg = "rule selection must be an iterable of selector values, not one string"
87
+ raise TypeError(msg)
88
+ parsed: set[RuleSelector] = set()
89
+ for value in values:
90
+ if isinstance(value, RuleSelector):
91
+ parsed.add(value)
92
+ else:
93
+ parsed.add(RuleSelector.parse(value))
94
+ return cls(frozenset(parsed))
95
+
96
+ @property
97
+ def engines(self) -> frozenset[RuleEngine]:
98
+ return frozenset(selector.engine for selector in self.selectors)
99
+
100
+ def ids_for(self, engine: RuleEngine) -> frozenset[RuleId]:
101
+ return frozenset(selector.rule_id for selector in self.selectors if selector.engine is engine)
102
+
103
+ def native_ids_for(self, engine: RuleEngine) -> frozenset[str]:
104
+ return frozenset(selector.native_rule_id for selector in self.selectors if selector.engine is engine)
105
+
106
+
107
+ class RuleCategory(StrEnum):
108
+ ARCHITECTURE = "architecture"
109
+ CORRECTNESS = "correctness"
110
+ MAINTAINABILITY = "maintainability"
111
+ PERFORMANCE = "performance"
112
+ SECURITY = "security"
113
+ STYLE = "style"
114
+ TESTING = "testing"
115
+
116
+
117
+ class DefaultLevel(StrEnum):
118
+ ERROR = "error"
119
+ OFF = "off"
120
+ WARNING = "warning"
121
+
122
+
123
+ class RuleStatus(StrEnum):
124
+ ACTIVE = "active"
125
+ RENAMED = "renamed"
126
+ RETIRED = "retired"
127
+
128
+
129
+ class AutofixPolicy(StrEnum):
130
+ NONE = "none"
131
+ SUGGESTION = "suggestion"
132
+ SAFE = "safe"
133
+
134
+
135
+ class ExpectedOutcome(StrEnum):
136
+ MATCH = "match"
137
+ NO_MATCH = "no-match"
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class ExampleFile:
142
+ path: PurePosixPath
143
+ source: str = field(repr=False)
144
+
145
+ def __post_init__(self) -> None:
146
+ if self.path.is_absolute() or ".." in self.path.parts or not self.path.name or "\\" in self.path.as_posix():
147
+ msg = "example file paths must be safe relative paths"
148
+ raise ValueError(msg)
149
+ if not self.source:
150
+ msg = "example file source must not be empty"
151
+ raise ValueError(msg)
152
+
153
+
154
+ @dataclass(frozen=True, slots=True)
155
+ class RuleExample:
156
+ example_id: str
157
+ outcome: ExpectedOutcome
158
+ files: tuple[ExampleFile, ...]
159
+ focus_path: PurePosixPath
160
+ expected_count: int
161
+ title: str
162
+ public: bool = False
163
+ fixed_files: tuple[ExampleFile, ...] = ()
164
+ scenario: str = "primary"
165
+
166
+ def __post_init__(self) -> None:
167
+ if not _KEBAB_CASE.fullmatch(self.example_id):
168
+ msg = "example ID must be non-empty lowercase kebab-case"
169
+ raise ValueError(msg)
170
+ if not _KEBAB_CASE.fullmatch(self.scenario):
171
+ msg = "example scenario must be non-empty lowercase kebab-case"
172
+ raise ValueError(msg)
173
+ if not self.title.strip():
174
+ msg = "example title must not be empty"
175
+ raise ValueError(msg)
176
+ paths = tuple(item.path for item in self.files)
177
+ if not paths or len(paths) != len(set(paths)):
178
+ msg = "example files must have unique paths"
179
+ raise ValueError(msg)
180
+ fixed_paths = tuple(item.path for item in self.fixed_files)
181
+ if len(fixed_paths) != len(set(fixed_paths)):
182
+ msg = "fixed example files must have unique paths"
183
+ raise ValueError(msg)
184
+ if self.focus_path not in paths:
185
+ msg = "example focus path must name one example file"
186
+ raise ValueError(msg)
187
+ if self.expected_count < 0:
188
+ msg = "example expected count must not be negative"
189
+ raise ValueError(msg)
190
+ if self.outcome is ExpectedOutcome.MATCH and self.expected_count < 1:
191
+ msg = "matching examples must expect at least one diagnostic"
192
+ raise ValueError(msg)
193
+ if self.outcome is ExpectedOutcome.NO_MATCH and self.expected_count != 0:
194
+ msg = "non-matching examples must expect zero diagnostics"
195
+ raise ValueError(msg)
196
+
197
+
198
+ @dataclass(frozen=True, slots=True)
199
+ class RuleSpec:
200
+ engine: RuleEngine
201
+ rule_id: RuleId
202
+ code: str | None
203
+ summary: str
204
+ rationale: str
205
+ remediation: str
206
+ category: RuleCategory
207
+ languages: frozenset[Language]
208
+ autofix: AutofixPolicy = AutofixPolicy.NONE
209
+ aliases: tuple[str, ...] = ()
210
+ examples: tuple[RuleExample, ...] = ()
211
+ limitations: tuple[str, ...] = ()
212
+ file_patterns: tuple[str, ...] = ()
213
+ message_ids: tuple[MessageId, ...] = ()
214
+ options_schema: str | None = None
215
+ references: tuple[str, ...] = ()
216
+ since: str | None = None
217
+
218
+ def __post_init__(self) -> None:
219
+ if not _KEBAB_CASE.fullmatch(self.rule_id):
220
+ msg = "rule ID must be non-empty lowercase kebab-case"
221
+ raise ValueError(msg)
222
+ if self.code is not None and not self.code.strip():
223
+ msg = "rule code must be non-empty when present"
224
+ raise ValueError(msg)
225
+ for label, value in (
226
+ ("summary", self.summary),
227
+ ("rationale", self.rationale),
228
+ ("remediation", self.remediation),
229
+ ):
230
+ if not value.strip():
231
+ msg = f"rule {label} must not be empty"
232
+ raise ValueError(msg)
233
+ if len(self.summary) > _MAX_SUMMARY_LENGTH or "\n" in self.summary:
234
+ msg = "rule summary must be one concise line of at most 160 characters"
235
+ raise ValueError(msg)
236
+ if not self.languages:
237
+ msg = "rule must name at least one language"
238
+ raise ValueError(msg)
239
+ if len(self.aliases) != len(set(self.aliases)) or any(
240
+ not _KEBAB_CASE.fullmatch(alias) or alias == self.rule_id for alias in self.aliases
241
+ ):
242
+ msg = "rule aliases must be unique historical lowercase kebab-case IDs"
243
+ raise ValueError(msg)
244
+ example_ids = tuple(example.example_id for example in self.examples)
245
+ if len(example_ids) != len(set(example_ids)):
246
+ msg = "rule example IDs must be unique"
247
+ raise ValueError(msg)
248
+ if self.autofix is AutofixPolicy.NONE and any(example.fixed_files for example in self.examples):
249
+ msg = "rules without autofix must not publish fixed example files"
250
+ raise ValueError(msg)
251
+ if len(self.message_ids) != len(set(self.message_ids)) or any(not value.strip() for value in self.message_ids):
252
+ msg = "rule message IDs must be unique and non-empty"
253
+ raise ValueError(msg)
254
+ if len(self.file_patterns) != len(set(self.file_patterns)) or any(
255
+ not pattern.strip() for pattern in self.file_patterns
256
+ ):
257
+ msg = "rule file patterns must be unique and non-empty"
258
+ raise ValueError(msg)
259
+ if any(not limitation.strip() for limitation in self.limitations):
260
+ msg = "rule limitations must not be empty"
261
+ raise ValueError(msg)
262
+ if any(not reference.startswith("https://") for reference in self.references):
263
+ msg = "rule references must use https"
264
+ raise ValueError(msg)
265
+ if self.since is not None and not self.since.strip():
266
+ msg = "rule since version must not be empty when present"
267
+ raise ValueError(msg)
268
+ if self.options_schema is not None:
269
+ try:
270
+ parsed_schema: object = json.loads(self.options_schema) # pyright: ignore[reportAny]
271
+ except json.JSONDecodeError as exc:
272
+ msg = "rule options schema must be valid JSON"
273
+ raise ValueError(msg) from exc
274
+ if not isinstance(parsed_schema, dict):
275
+ msg = "rule options schema must be a JSON object"
276
+ raise ValueError(msg)
277
+ public_scenarios = {example.scenario for example in self.examples if example.public}
278
+ for scenario in public_scenarios:
279
+ pair = tuple(example for example in self.examples if example.public and example.scenario == scenario)
280
+ if len(pair) != _PUBLIC_PAIR_SIZE or {example.outcome for example in pair} != {
281
+ ExpectedOutcome.MATCH,
282
+ ExpectedOutcome.NO_MATCH,
283
+ }:
284
+ msg = f"published example scenario {scenario!r} must contain both matching and non-matching cases exactly once"
285
+ raise ValueError(msg)
286
+
287
+ @property
288
+ def key(self) -> str:
289
+ """Return the collision-free, engine-qualified rule identity."""
290
+ return f"{self.engine.value}:{self.rule_id}"
291
+
292
+
293
+ @dataclass(frozen=True, slots=True)
294
+ class RuleProblem:
295
+ key: str
296
+ summary: str
297
+ harm: str
298
+ languages: frozenset[Language]
299
+ bad_examples: tuple[str, ...]
300
+ good_examples: tuple[str, ...]
301
+ exclusions: tuple[str, ...] = ()
302
+ autofix: AutofixPolicy = AutofixPolicy.NONE
303
+
304
+ def __post_init__(self) -> None:
305
+ if not self.key or not self.key.replace("-", "").isalnum() or self.key != self.key.lower():
306
+ msg = "problem key must be non-empty lowercase kebab-case"
307
+ raise ValueError(msg)
308
+ for label, value in (("summary", self.summary), ("harm", self.harm)):
309
+ if not value.strip():
310
+ msg = f"problem {label} must not be empty"
311
+ raise ValueError(msg)
312
+ if not self.languages:
313
+ msg = "problem must name at least one language"
314
+ raise ValueError(msg)
315
+ if not self.bad_examples or not self.good_examples:
316
+ msg = "problem must contain both bad and good examples"
317
+ raise ValueError(msg)
318
+
319
+
320
+ @dataclass(frozen=True, slots=True)
321
+ class CatalogRule:
322
+ identifier: str
323
+ summary: str
324
+ origin: RuleOrigin
325
+ languages: frozenset[Language]
326
+ configurable: bool = False
327
+ documentation: str | None = None
328
+
329
+ def __post_init__(self) -> None:
330
+ if not self.identifier.strip() or not self.summary.strip():
331
+ msg = "catalog rule identifier and summary must not be empty"
332
+ raise ValueError(msg)
333
+ if not self.languages:
334
+ msg = "catalog rule must name at least one language"
335
+ raise ValueError(msg)
336
+ if self.documentation is not None and not self.documentation.startswith("https://"):
337
+ msg = "catalog documentation must use https"
338
+ raise ValueError(msg)
339
+
340
+
341
+ @dataclass(frozen=True, slots=True)
342
+ class Finding:
343
+ rule_id: str
344
+ line: int
345
+ column: int
346
+ message: str
347
+
348
+ def __post_init__(self) -> None:
349
+ if not self.rule_id.strip() or not self.message.strip():
350
+ msg = "finding rule ID and message must not be empty"
351
+ raise ValueError(msg)
352
+ if self.line < 1 or self.column < 1:
353
+ msg = "finding locations are one-based"
354
+ raise ValueError(msg)
355
+
356
+
357
+ @dataclass(frozen=True, slots=True)
358
+ class EvaluationCase:
359
+ case_id: str
360
+ language: Language
361
+ source: str = field(repr=False)
362
+ expected: ExpectedOutcome = ExpectedOutcome.NO_MATCH
363
+ path: PurePosixPath = _DEFAULT_CASE_PATH
364
+ private: bool = False
365
+
366
+ def __post_init__(self) -> None:
367
+ if not self.case_id.strip() or not self.source:
368
+ msg = "evaluation case ID and source must not be empty"
369
+ raise ValueError(msg)
370
+ if self.path.is_absolute() or ".." in self.path.parts:
371
+ msg = "evaluation case paths must be safe relative paths"
372
+ raise ValueError(msg)
373
+
374
+ @property
375
+ def report_path(self) -> str:
376
+ """Return a stable path that never reveals a private corpus layout."""
377
+ return "<private>" if self.private else self.path.as_posix()
378
+
379
+ @property
380
+ def report_id(self) -> str:
381
+ """Redact private case labels before they enter a report."""
382
+ return "<private>" if self.private else self.case_id