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,225 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from importlib import import_module
6
+ import json
7
+ from operator import itemgetter
8
+ import re
9
+ from typing import TYPE_CHECKING, Final, NamedTuple, Protocol
10
+
11
+ from sarj_standards.libs.adoption.manifest import as_table, list_field
12
+ from sarj_standards.libs.linting import textlint
13
+
14
+ from . import ledger, repository
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+
21
+ _FAMILIES: Final = (
22
+ ("python", "sarj_python_lint.rules", "packages/python"),
23
+ ("sql", "sarj_sql_lint.rules", "packages/sql"),
24
+ ("iac", "sarj_iac_lint.rules", "packages/iac"),
25
+ )
26
+ _RENAME_ENTRY: Final = re.compile(r'^\s*"(?P<old>[a-z0-9-]+)": "(?P<new>[a-z0-9-]+)",', re.MULTILINE)
27
+ _PLACEHOLDER: Final = "TODO: say why it went, in one line a consumer can act on"
28
+
29
+
30
+ class Rule(Protocol):
31
+ code: str
32
+ __module__: str
33
+ documentation: RuleDocumentation | None
34
+
35
+
36
+ class RuleDocumentation(Protocol):
37
+ aliases: tuple[str, ...]
38
+
39
+
40
+ class _NativeLedgerState(NamedTuple):
41
+ rules: dict[str, list[str]]
42
+ codes: dict[str, list[str]]
43
+ renames: dict[tuple[str, str], str]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class SyncResult:
48
+ status: int
49
+ message: str
50
+
51
+
52
+ def inventory(root: Path) -> list[dict[str, str]]:
53
+ items: list[dict[str, str]] = []
54
+ for family, module_name, package in _FAMILIES:
55
+ registry = _registry(module_name)
56
+ for rule_id, rule in sorted(registry.items()):
57
+ module = rule.__module__.replace(".", "/")
58
+ source_name = module.rsplit("/", 1)[-1]
59
+ items.append(
60
+ {
61
+ "family": family,
62
+ "id": rule_id,
63
+ "code": str(rule.code),
64
+ "source": f"{package}/src/{module}.py",
65
+ "test": f"{package}/tests/rules/test_{source_name}.py",
66
+ }
67
+ )
68
+ for rule_id, meta in sorted(textlint.REGISTRY.items()):
69
+ items.append(
70
+ {
71
+ "family": "text",
72
+ "id": rule_id,
73
+ "code": meta.code,
74
+ "source": "packages/standards/src/sarj_standards/libs/linting/textlint.py",
75
+ "test": "packages/standards/tests/test_textlint.py",
76
+ }
77
+ )
78
+ eslint_rules = (
79
+ repository.eslint_rule_names(root)
80
+ if (root / "packages/typescript/src/index.ts").is_file()
81
+ else list(ledger.load().rules.get(ledger.ESLINT, ()))
82
+ )
83
+ items.extend(
84
+ {
85
+ "family": "typescript",
86
+ "id": rule_id,
87
+ "code": f"@sarj/{rule_id}",
88
+ "source": f"packages/typescript/src/rules/{rule_id}.ts",
89
+ "test": f"packages/typescript/tests/rules/{rule_id}.test.ts",
90
+ }
91
+ for rule_id in eslint_rules
92
+ )
93
+ return sorted(items, key=itemgetter("family", "id"))
94
+
95
+
96
+ def sync_ledger(root: Path, *, check: bool) -> SyncResult:
97
+ path = root / "packages/standards/src/sarj_standards/configs/rule-ledger.json"
98
+ previous = _load_ledger(path)
99
+ rules, codes, source_renames = _native_ledger_state()
100
+ rules["eslint"] = repository.eslint_rule_names(root)
101
+ rules["text"] = sorted(textlint.REGISTRY)
102
+ codes["text"] = sorted(meta.code for meta in textlint.REGISTRY.values())
103
+ for rule_id, meta in textlint.REGISTRY.items():
104
+ for old_id in meta.aliases:
105
+ source_renames["text", old_id] = rule_id
106
+ retired = _retired(previous, root, source_renames)
107
+ known = {str(entry.get("id")) for entry in retired}
108
+ old_rules = repository_table(previous.get("rules"))
109
+ old_codes = repository_table(previous.get("codes"))
110
+ for family, raw_names in old_rules.items():
111
+ prefix = "@sarj/" if family == "eslint" else ""
112
+ for name in _string_list(raw_names):
113
+ identifier = f"{prefix}{name}"
114
+ if name not in rules.get(family, []) and identifier not in known:
115
+ retired.append(_retired_entry(identifier, family))
116
+ known.add(identifier)
117
+ for family, raw_codes in old_codes.items():
118
+ for code in _string_list(raw_codes):
119
+ if code not in codes.get(family, []) and code not in known:
120
+ retired.append(_retired_entry(code, "code"))
121
+ known.add(code)
122
+ updated = {
123
+ "$comment": previous.get("$comment", "Rule compatibility ledger."),
124
+ "rules": rules,
125
+ "codes": codes,
126
+ "retired": sorted(retired, key=lambda entry: str(entry.get("id"))),
127
+ }
128
+ rendered = json.dumps(updated, indent=2) + "\n"
129
+ current = path.read_text(encoding="utf-8")
130
+ if rendered == current:
131
+ return SyncResult(0, f"ok: {path.name} matches the registries")
132
+ if check:
133
+ return SyncResult(1, f"drift: {path}")
134
+ path.write_text(rendered, encoding="utf-8")
135
+ status = 1 if any(entry.get("note") == _PLACEHOLDER for entry in retired) else 0
136
+ return SyncResult(status, f"wrote: {path}")
137
+
138
+
139
+ def _native_ledger_state() -> _NativeLedgerState:
140
+ rules: dict[str, list[str]] = {}
141
+ codes: dict[str, list[str]] = {}
142
+ renames: dict[tuple[str, str], str] = {}
143
+ for family, module_name, _package in _FAMILIES:
144
+ registry = _registry(module_name)
145
+ rules[family] = sorted(registry)
146
+ codes[family] = sorted(str(rule.code) for rule in registry.values())
147
+ for rule_id, rule in registry.items():
148
+ if rule.documentation is None:
149
+ continue
150
+ for old_id in rule.documentation.aliases:
151
+ renames[family, old_id] = rule_id
152
+ return _NativeLedgerState(rules, codes, renames)
153
+
154
+
155
+ def repository_table(value: object) -> dict[str, object]:
156
+ return as_table(value)
157
+
158
+
159
+ def _load_ledger(path: Path) -> dict[str, object]:
160
+ value: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
161
+ return repository_table(value)
162
+
163
+
164
+ def _registry(module_name: str) -> Mapping[str, type[Rule]]:
165
+ module = import_module(module_name)
166
+ value = getattr(module, "REGISTRY", None)
167
+ if not isinstance(value, Mapping):
168
+ msg = f"{module_name} has no registry"
169
+ raise TypeError(msg)
170
+ return value # pyright: ignore[reportUnknownVariableType]
171
+
172
+
173
+ def _retired(
174
+ previous: Mapping[str, object],
175
+ root: Path,
176
+ source_renames: Mapping[tuple[str, str], str],
177
+ ) -> list[dict[str, object]]:
178
+ entries = [repository_table(item) for item in _object_list(previous.get("retired"))]
179
+ derived_kinds = {"eslint", *(kind for kind, _old_id in source_renames)}
180
+ kept = [entry for entry in entries if not (entry.get("kind") in derived_kinds and entry.get("status") == "renamed")]
181
+ existing = {str(entry.get("id")): entry for entry in entries}
182
+ for (kind, old), new in sorted(source_renames.items()):
183
+ prior = existing.get(old)
184
+ kept.append(
185
+ prior
186
+ if prior is not None and prior.get("replacement") == new
187
+ else {
188
+ "id": old,
189
+ "kind": kind,
190
+ "status": "renamed",
191
+ "replacement": new,
192
+ "note": f"Replace sarj-{old} with sarj-{new} before upgrading.",
193
+ }
194
+ )
195
+ renames_path = root / "packages/typescript/src/rules/_renames.ts"
196
+ for match in _RENAME_ENTRY.finditer(renames_path.read_text(encoding="utf-8")):
197
+ old = match.group("old")
198
+ new = match.group("new")
199
+ identifier = f"@sarj/{old}"
200
+ replacement = f"@sarj/{new}"
201
+ prior = existing.get(identifier)
202
+ kept.append(
203
+ prior
204
+ if prior is not None and prior.get("replacement") == replacement
205
+ else {
206
+ "id": identifier,
207
+ "kind": "eslint",
208
+ "status": "renamed",
209
+ "replacement": replacement,
210
+ "note": f"Replace @sarj/{old} with @sarj/{new} before upgrading.",
211
+ }
212
+ )
213
+ return kept
214
+
215
+
216
+ def _retired_entry(identifier: str, kind: str) -> dict[str, object]:
217
+ return {"id": identifier, "kind": kind, "status": "removed", "replacement": None, "note": _PLACEHOLDER}
218
+
219
+
220
+ def _object_list(value: object) -> list[object]:
221
+ return list_field({"value": value}, "value")
222
+
223
+
224
+ def _string_list(value: object) -> list[str]:
225
+ return [item for item in _object_list(value) if isinstance(item, str)]
@@ -0,0 +1,74 @@
1
+ from .catalog import DocumentedRule, RuleCatalog, RuleCatalogDocument
2
+ from .contracts import (
3
+ AutofixPolicy,
4
+ CatalogRule,
5
+ DefaultLevel,
6
+ EvaluationCase,
7
+ ExampleFile,
8
+ ExpectedOutcome,
9
+ Finding,
10
+ Language,
11
+ MessageId,
12
+ RuleCategory,
13
+ RuleEngine,
14
+ RuleExample,
15
+ RuleId,
16
+ RuleOrigin,
17
+ RuleProblem,
18
+ RuleSelection,
19
+ RuleSelector,
20
+ RuleSpec,
21
+ RuleStatus,
22
+ )
23
+ from .corpus_runner import (
24
+ CorpusBatchResult,
25
+ CorpusLintError,
26
+ IsolatedCorpusReport,
27
+ run_isolated_corpora,
28
+ )
29
+ from .evaluation import (
30
+ CaseEvaluation,
31
+ EvaluationEvidence,
32
+ EvaluationReport,
33
+ EvaluationThresholds,
34
+ PromotionDecision,
35
+ RuleEvaluator,
36
+ evaluate,
37
+ )
38
+
39
+
40
+ __all__ = [
41
+ "AutofixPolicy",
42
+ "CaseEvaluation",
43
+ "CatalogRule",
44
+ "CorpusBatchResult",
45
+ "CorpusLintError",
46
+ "DefaultLevel",
47
+ "DocumentedRule",
48
+ "EvaluationCase",
49
+ "EvaluationEvidence",
50
+ "EvaluationReport",
51
+ "EvaluationThresholds",
52
+ "ExampleFile",
53
+ "ExpectedOutcome",
54
+ "Finding",
55
+ "IsolatedCorpusReport",
56
+ "Language",
57
+ "MessageId",
58
+ "PromotionDecision",
59
+ "RuleCatalog",
60
+ "RuleCatalogDocument",
61
+ "RuleCategory",
62
+ "RuleEngine",
63
+ "RuleEvaluator",
64
+ "RuleExample",
65
+ "RuleId",
66
+ "RuleOrigin",
67
+ "RuleProblem",
68
+ "RuleSelection",
69
+ "RuleSelector",
70
+ "RuleSpec",
71
+ "RuleStatus",
72
+ "evaluate",
73
+ "run_isolated_corpora",
74
+ ]
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, Self
7
+
8
+ from .contracts import (
9
+ CatalogRule,
10
+ DefaultLevel,
11
+ ExpectedOutcome,
12
+ Language,
13
+ RuleOrigin,
14
+ RuleSpec,
15
+ RuleStatus,
16
+ )
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Iterable
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class RuleCatalog:
25
+ rules: tuple[CatalogRule, ...]
26
+
27
+ @classmethod
28
+ def from_rules(cls, rules: Iterable[CatalogRule]) -> Self:
29
+ collected = tuple(rules)
30
+ identifiers = [rule.identifier for rule in collected]
31
+ if len(identifiers) != len(set(identifiers)):
32
+ msg = "catalog rule identifiers must be unique"
33
+ raise ValueError(msg)
34
+ return cls(tuple(sorted(collected, key=lambda rule: rule.identifier)))
35
+
36
+ def get(self, identifier: str) -> CatalogRule | None:
37
+ return next((rule for rule in self.rules if rule.identifier == identifier), None)
38
+
39
+ def filter(
40
+ self,
41
+ *,
42
+ language: Language | None = None,
43
+ origin: RuleOrigin | None = None,
44
+ configurable: bool | None = None,
45
+ ) -> tuple[CatalogRule, ...]:
46
+ return tuple(
47
+ rule
48
+ for rule in self.rules
49
+ if (language is None or language in rule.languages)
50
+ and (origin is None or rule.origin is origin)
51
+ and (configurable is None or rule.configurable is configurable)
52
+ )
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class DocumentedRule:
57
+ spec: RuleSpec
58
+ default_level: DefaultLevel
59
+ source: PurePosixPath
60
+ test: PurePosixPath
61
+ status: RuleStatus = RuleStatus.ACTIVE
62
+
63
+ def __post_init__(self) -> None:
64
+ if self.status is not RuleStatus.ACTIVE:
65
+ msg = "documented live rules must have active status; lifecycle tombstones are separate"
66
+ raise ValueError(msg)
67
+ for path in (self.source, self.test):
68
+ if path.is_absolute() or ".." in path.parts or not path.name or "\\" in path.as_posix():
69
+ msg = "catalog source and test paths must be safe repository-relative paths"
70
+ raise ValueError(msg)
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class RuleCatalogDocument:
75
+ rules: tuple[DocumentedRule, ...]
76
+ schema_version: int = 1
77
+
78
+ def __post_init__(self) -> None:
79
+ if self.schema_version != 1:
80
+ msg = "unsupported rule catalog schema version"
81
+ raise ValueError(msg)
82
+ keys = tuple(rule.spec.key for rule in self.rules)
83
+ if len(keys) != len(set(keys)):
84
+ msg = "catalog rule keys must be unique"
85
+ raise ValueError(msg)
86
+ aliases = tuple(f"{rule.spec.engine.value}:{alias}" for rule in self.rules for alias in rule.spec.aliases)
87
+ if len(aliases) != len(set(aliases)) or set(aliases) & set(keys):
88
+ msg = "catalog aliases must be unique and must not shadow live rule keys"
89
+ raise ValueError(msg)
90
+
91
+ def as_public_dict(self) -> dict[str, object]:
92
+ return {
93
+ "schemaVersion": self.schema_version,
94
+ "rules": [self._rule_dict(rule) for rule in sorted(self.rules, key=lambda item: item.spec.key)],
95
+ }
96
+
97
+ @staticmethod
98
+ def _rule_dict(rule: DocumentedRule) -> dict[str, object]:
99
+ spec = rule.spec
100
+ examples = sorted(
101
+ (example for example in spec.examples if example.public),
102
+ key=lambda example: example.example_id,
103
+ )
104
+ return {
105
+ "key": spec.key,
106
+ "engine": spec.engine.value,
107
+ "id": spec.rule_id,
108
+ "code": spec.code,
109
+ "summary": spec.summary,
110
+ "rationale": spec.rationale,
111
+ "remediation": spec.remediation,
112
+ "category": spec.category.value,
113
+ "languages": sorted(language.value for language in spec.languages),
114
+ "defaultLevel": rule.default_level.value,
115
+ "autofix": spec.autofix.value,
116
+ "status": rule.status.value,
117
+ "aliases": sorted(spec.aliases),
118
+ "limitations": list(spec.limitations),
119
+ "filePatterns": list(spec.file_patterns),
120
+ "messageIds": list(spec.message_ids),
121
+ "optionsSchema": json.loads(spec.options_schema) if spec.options_schema is not None else None,
122
+ "references": list(spec.references),
123
+ "since": spec.since,
124
+ "source": rule.source.as_posix(),
125
+ "test": rule.test.as_posix(),
126
+ "examples": [
127
+ {
128
+ "id": example.example_id,
129
+ "scenarioId": example.scenario,
130
+ "title": example.title,
131
+ "outcome": "reject" if example.outcome is ExpectedOutcome.MATCH else "accept",
132
+ "focusPath": example.focus_path.as_posix(),
133
+ "expectedCount": example.expected_count,
134
+ "files": [
135
+ {"path": item.path.as_posix(), "source": item.source}
136
+ for item in sorted(example.files, key=lambda item: item.path)
137
+ ],
138
+ "fixedFiles": [
139
+ {"path": item.path.as_posix(), "source": item.source}
140
+ for item in sorted(example.fixed_files, key=lambda item: item.path)
141
+ ],
142
+ }
143
+ for example in examples
144
+ ],
145
+ }