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,422 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from importlib import import_module
5
+ from pathlib import Path
6
+ import time
7
+ from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
8
+
9
+ from sarj_standards.libs.diagnostics import (
10
+ AnalysisReport,
11
+ AnalyzerId,
12
+ Completion,
13
+ Conclusion,
14
+ Diagnostic,
15
+ ExecutionIssue,
16
+ InvocationId,
17
+ Location,
18
+ Severity,
19
+ SourceDocument,
20
+ ToolReport,
21
+ diagnostic_fingerprint,
22
+ )
23
+ from sarj_standards.libs.rules import RuleEngine, RuleSelection
24
+
25
+ from . import textlint
26
+ from .runner import GroupedPaths, group_paths
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Mapping, Sequence
31
+
32
+ from .policy import Policy
33
+
34
+
35
+ @runtime_checkable
36
+ class _NativeDiagnostic(Protocol):
37
+ @property
38
+ def path(self) -> Path: ...
39
+
40
+ @property
41
+ def line(self) -> int: ...
42
+
43
+ @property
44
+ def col(self) -> int: ...
45
+
46
+ @property
47
+ def code(self) -> str: ...
48
+
49
+ @property
50
+ def message(self) -> str: ...
51
+
52
+ @property
53
+ def column_encoding(self) -> object: ...
54
+
55
+
56
+ @runtime_checkable
57
+ class _SeverityDiagnostic(_NativeDiagnostic, Protocol):
58
+ @property
59
+ def severity(self) -> object: ...
60
+
61
+
62
+ class _RuleMetadata(Protocol):
63
+ code: str
64
+ id: str
65
+ description: str
66
+
67
+
68
+ @runtime_checkable
69
+ class _CheckerModule(Protocol):
70
+ def analyze(self, rule_ids: list[str], paths: list[Path]) -> list[_NativeDiagnostic]: ...
71
+
72
+
73
+ @runtime_checkable
74
+ class _RegistryModule(Protocol):
75
+ REGISTRY: Mapping[str, type[_RuleMetadata]]
76
+
77
+
78
+ class _LoadedNative(NamedTuple):
79
+ checker: _CheckerModule
80
+ registry: _RegistryModule
81
+
82
+
83
+ class _NativeRuleMetadata(NamedTuple):
84
+ rule_id: str
85
+ description: str
86
+
87
+
88
+ def analyze(
89
+ files: Sequence[str],
90
+ *,
91
+ root: Path,
92
+ python_baseline: Path | None = None,
93
+ policy: Policy | None = None,
94
+ grouped: GroupedPaths | None = None,
95
+ rule_selection: RuleSelection | None = None,
96
+ ) -> AnalysisReport:
97
+ root = root.resolve()
98
+ try:
99
+ contained = tuple(_contained_path(item, root) for item in files)
100
+ routed = grouped if grouped is not None else group_paths(contained, policy=policy)
101
+ except (OSError, ValueError) as exc:
102
+ issue = ExecutionIssue("sarj-standards", "invalid-input", str(exc).replace(str(root), "."))
103
+ tool = ToolReport("sarj-standards", Completion.FAILED, issues=(issue,))
104
+ return AnalysisReport(root, Completion.FAILED, Conclusion.INCONCLUSIVE, (tool,))
105
+
106
+ reports = tuple(
107
+ report
108
+ for report in (
109
+ _native_report(
110
+ "sarj-python-lint",
111
+ "sarj_python_lint",
112
+ routed.python,
113
+ root,
114
+ python_baseline=python_baseline,
115
+ rule_ids=_selected_ids(rule_selection, RuleEngine.PYTHON),
116
+ ),
117
+ _native_report(
118
+ "sarj-sql-lint",
119
+ "sarj_sql_lint",
120
+ routed.sql,
121
+ root,
122
+ rule_ids=_selected_ids(rule_selection, RuleEngine.SQL),
123
+ ),
124
+ _native_report(
125
+ "sarj-iac-lint",
126
+ "sarj_iac_lint",
127
+ routed.iac,
128
+ root,
129
+ rule_ids=_selected_ids(rule_selection, RuleEngine.IAC),
130
+ ),
131
+ _text_report(
132
+ routed,
133
+ root,
134
+ rule_ids=_selected_ids(rule_selection, RuleEngine.TEXT),
135
+ ),
136
+ )
137
+ if report is not None
138
+ )
139
+ report = report_from_tools(root, reports)
140
+ if policy is None:
141
+ return report
142
+ filtered = tuple(
143
+ ToolReport(
144
+ item.name,
145
+ item.completion,
146
+ diagnostics=policy.filter_diagnostics(item.diagnostics),
147
+ issues=item.issues,
148
+ analyzer_id=item.analyzer_id,
149
+ invocation_id=item.invocation_id,
150
+ version=item.version,
151
+ duration_ms=item.duration_ms,
152
+ file_count=item.file_count,
153
+ cache_status=item.cache_status,
154
+ )
155
+ for item in report.tools
156
+ )
157
+ return report_from_tools(root, filtered)
158
+
159
+
160
+ def _selected_ids(selection: RuleSelection | None, engine: RuleEngine) -> frozenset[str] | None:
161
+ if selection is None:
162
+ return None
163
+ return frozenset(str(value) for value in selection.ids_for(engine))
164
+
165
+
166
+ def _contained_path(value: str, root: Path) -> str:
167
+ path = Path(value)
168
+ resolved = (path if path.is_absolute() else root / path).resolve()
169
+ try:
170
+ resolved.relative_to(root)
171
+ except ValueError as exc:
172
+ msg = "analysis path is outside the repository root"
173
+ raise ValueError(msg) from exc
174
+ return str(resolved)
175
+
176
+
177
+ def report_from_tools(root: Path, reports: Sequence[ToolReport]) -> AnalysisReport:
178
+ normalized = tuple(
179
+ ToolReport(
180
+ report.name,
181
+ report.completion,
182
+ diagnostics=tuple(
183
+ sorted(
184
+ (
185
+ item
186
+ if item.fingerprint is not None
187
+ else replace(item, fingerprint=diagnostic_fingerprint(item, anchor=item.message))
188
+ for item in report.diagnostics
189
+ ),
190
+ key=_diagnostic_key,
191
+ )
192
+ ),
193
+ issues=tuple(sorted(report.issues, key=lambda issue: (issue.source, issue.kind, issue.message))),
194
+ analyzer_id=report.analyzer_id,
195
+ invocation_id=report.invocation_id,
196
+ version=report.version,
197
+ duration_ms=report.duration_ms,
198
+ file_count=report.file_count,
199
+ cache_status=report.cache_status,
200
+ )
201
+ for report in sorted(reports, key=lambda report: report.name)
202
+ )
203
+ issues = tuple(issue for report in normalized for issue in report.issues)
204
+ diagnostics = tuple(item for report in normalized for item in report.diagnostics)
205
+ if issues:
206
+ completion = (
207
+ Completion.FAILED
208
+ if normalized and all(report.completion is Completion.FAILED for report in normalized)
209
+ else Completion.PARTIAL
210
+ )
211
+ conclusion = Conclusion.FINDINGS if diagnostics else Conclusion.INCONCLUSIVE
212
+ else:
213
+ completion = Completion.COMPLETE
214
+ conclusion = Conclusion.FINDINGS if diagnostics else Conclusion.PASSED
215
+ return AnalysisReport(root, completion, conclusion, normalized)
216
+
217
+
218
+ def _diagnostic_key(diagnostic: Diagnostic) -> tuple[object, ...]:
219
+ location = diagnostic.location
220
+ position = location.region.start if location.region is not None else location.position
221
+ end = location.region.end if location.region is not None else None
222
+ return (
223
+ diagnostic.source,
224
+ diagnostic.code,
225
+ location.path,
226
+ -1 if position is None else position.line,
227
+ -1 if position is None else position.character,
228
+ -1 if end is None else end.line,
229
+ -1 if end is None else end.character,
230
+ diagnostic.severity.value,
231
+ diagnostic.message,
232
+ )
233
+
234
+
235
+ def _native_report(
236
+ name: str,
237
+ package: str,
238
+ files: Sequence[str],
239
+ root: Path,
240
+ *,
241
+ python_baseline: Path | None = None,
242
+ rule_ids: frozenset[str] | None = None,
243
+ ) -> ToolReport | None:
244
+ if not files or rule_ids == frozenset():
245
+ return None
246
+ try:
247
+ return _run_native(name, package, files, root, python_baseline=python_baseline, rule_ids=rule_ids)
248
+ except Exception as exc: # ruff: ignore[blind-except] -- one analyzer failure must not erase other tool results.
249
+ issue = ExecutionIssue(name, "analyzer-failure", f"{type(exc).__name__}: {exc}")
250
+ return ToolReport(name, Completion.FAILED, issues=(issue,))
251
+
252
+
253
+ def _run_native(
254
+ name: str,
255
+ package: str,
256
+ files: Sequence[str],
257
+ root: Path,
258
+ *,
259
+ python_baseline: Path | None,
260
+ rule_ids: frozenset[str] | None,
261
+ ) -> ToolReport:
262
+ started = time.monotonic()
263
+ checker_module, registry_module = _load_native(package)
264
+ metadata = _metadata(registry_module.REGISTRY)
265
+ selected_rules = sorted(registry_module.REGISTRY) if rule_ids is None else sorted(rule_ids)
266
+ unknown = sorted(set(selected_rules) - registry_module.REGISTRY.keys())
267
+ if unknown:
268
+ msg = f"unknown {package} rule(s): {', '.join(unknown)}"
269
+ raise ValueError(msg)
270
+ paths = [Path(item) for item in files]
271
+ native_result: Sequence[_NativeDiagnostic]
272
+ if package == "sarj_python_lint":
273
+ from sarj_python_lint.__main__ import analyze as analyze_python # ruff: ignore[import-outside-top-level]
274
+
275
+ native_result = analyze_python(
276
+ selected_rules,
277
+ paths,
278
+ baseline=python_baseline,
279
+ root=root,
280
+ )
281
+ else:
282
+ native_result = checker_module.analyze(selected_rules, paths)
283
+ cache: dict[Path, SourceDocument | None] = {}
284
+ diagnostics = tuple(
285
+ _normalize_native(item, source=name, root=root, metadata=metadata, documents=cache) for item in native_result
286
+ )
287
+ return ToolReport(
288
+ name,
289
+ Completion.COMPLETE,
290
+ diagnostics=diagnostics,
291
+ analyzer_id=AnalyzerId(name),
292
+ invocation_id=InvocationId(name),
293
+ duration_ms=round((time.monotonic() - started) * 1_000),
294
+ file_count=len(files),
295
+ )
296
+
297
+
298
+ def _load_native(package: str) -> _LoadedNative:
299
+ checker_module = import_module(f"{package}.__main__")
300
+ registry_module = import_module(f"{package}.rules")
301
+ if not isinstance(checker_module, _CheckerModule) or not isinstance(registry_module, _RegistryModule):
302
+ msg = f"{package} does not expose the expected in-process analysis API"
303
+ raise TypeError(msg)
304
+ return _LoadedNative(checker_module, registry_module)
305
+
306
+
307
+ def _metadata(registry: Mapping[str, type[_RuleMetadata]]) -> dict[str, _NativeRuleMetadata]:
308
+ by_code: dict[str, _NativeRuleMetadata] = {}
309
+ for rule_id, rule_type in registry.items():
310
+ rule = rule_type()
311
+ by_code[rule.code] = _NativeRuleMetadata(rule_id, rule.description)
312
+ return by_code
313
+
314
+
315
+ def _normalize_native(
316
+ item: _NativeDiagnostic,
317
+ *,
318
+ source: str,
319
+ root: Path,
320
+ metadata: Mapping[str, _NativeRuleMetadata],
321
+ documents: dict[Path, SourceDocument | None],
322
+ ) -> Diagnostic:
323
+ resolved = item.path.resolve()
324
+ document = documents.get(resolved)
325
+ if resolved not in documents:
326
+ try:
327
+ document = SourceDocument.read(resolved)
328
+ except OSError:
329
+ document = None
330
+ documents[resolved] = document
331
+ position = None
332
+ if document is not None:
333
+ position = (
334
+ document.byte_point(line=item.line, column=item.col)
335
+ if source == "sarj-python-lint" and str(item.column_encoding) == "utf8-bytes"
336
+ else document.point(line=item.line, column=item.col)
337
+ )
338
+ rule_id, help_text = metadata.get(item.code, (None, None))
339
+ return Diagnostic(
340
+ code=item.code,
341
+ rule_id=rule_id,
342
+ message=item.message,
343
+ severity=_severity(item),
344
+ source=source,
345
+ location=Location(_relative_path(resolved, root), position=position),
346
+ help=help_text,
347
+ )
348
+
349
+
350
+ def _severity(item: _NativeDiagnostic) -> Severity:
351
+ if isinstance(item, _SeverityDiagnostic) and str(item.severity) == "warning":
352
+ return Severity.WARNING
353
+ return Severity.ERROR
354
+
355
+
356
+ def _text_report(
357
+ grouped: GroupedPaths,
358
+ root: Path,
359
+ *,
360
+ rule_ids: frozenset[str] | None = None,
361
+ ) -> ToolReport | None:
362
+ if not grouped.text or rule_ids == frozenset():
363
+ return None
364
+ try:
365
+ return _run_text(grouped.text, root, rule_ids=rule_ids)
366
+ except Exception as exc: # ruff: ignore[blind-except] -- retain results from independent analyzers.
367
+ issue = ExecutionIssue("sarj-text-lint", "analyzer-failure", f"{type(exc).__name__}: {exc}")
368
+ return ToolReport("sarj-text-lint", Completion.FAILED, issues=(issue,))
369
+
370
+
371
+ def _run_text(files: Sequence[str], root: Path, *, rule_ids: frozenset[str] | None) -> ToolReport:
372
+ selected = frozenset(textlint.REGISTRY) if rule_ids is None else rule_ids
373
+ unknown = sorted(selected - textlint.REGISTRY.keys())
374
+ if unknown:
375
+ msg = f"unknown text rule(s): {', '.join(unknown)}"
376
+ raise ValueError(msg)
377
+ raw = tuple(
378
+ finding
379
+ for finding in textlint.check_paths(files, root=root)
380
+ if any(meta.code == finding.code and rule_id in selected for rule_id, meta in textlint.REGISTRY.items())
381
+ )
382
+ by_code = {meta.code: (rule_id, meta.description, meta.blocking) for rule_id, meta in textlint.REGISTRY.items()}
383
+ documents: dict[Path, SourceDocument | None] = {}
384
+ diagnostics = tuple(_normalize_text(item, root, by_code, documents) for item in raw)
385
+ return ToolReport("sarj-text-lint", Completion.COMPLETE, diagnostics=diagnostics)
386
+
387
+
388
+ def _normalize_text(
389
+ item: textlint.Finding,
390
+ root: Path,
391
+ metadata: Mapping[str, tuple[str, str, bool]],
392
+ documents: dict[Path, SourceDocument | None],
393
+ ) -> Diagnostic:
394
+ resolved = item.path.resolve()
395
+ document = documents.get(resolved)
396
+ if resolved not in documents:
397
+ try:
398
+ document = SourceDocument.read(resolved)
399
+ except OSError:
400
+ document = None
401
+ documents[resolved] = document
402
+ rule_id, help_text, blocking = metadata[item.code]
403
+ return Diagnostic(
404
+ code=item.code,
405
+ rule_id=rule_id,
406
+ message=item.message,
407
+ severity=Severity.ERROR if blocking else Severity.WARNING,
408
+ source="sarj-text-lint",
409
+ location=Location(
410
+ _relative_path(resolved, root),
411
+ position=None if document is None else document.point(line=item.line, column=1),
412
+ ),
413
+ help=help_text,
414
+ )
415
+
416
+
417
+ def _relative_path(path: Path, root: Path) -> str:
418
+ try:
419
+ return path.relative_to(root).as_posix()
420
+ except ValueError as exc:
421
+ msg = "analyzer reported a path outside the repository root"
422
+ raise ValueError(msg) from exc