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.
- code_standards-7.0.0.dist-info/METADATA +53 -0
- code_standards-7.0.0.dist-info/RECORD +99 -0
- code_standards-7.0.0.dist-info/WHEEL +4 -0
- code_standards-7.0.0.dist-info/entry_points.txt +3 -0
- code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
- sarj_standards/__init__.py +30 -0
- sarj_standards/__main__.py +5 -0
- sarj_standards/_meta.py +22 -0
- sarj_standards/api.py +890 -0
- sarj_standards/cli/__init__.py +0 -0
- sarj_standards/cli/main.py +2466 -0
- sarj_standards/configs/cli-reference.v1.json +1 -0
- sarj_standards/configs/doctor.config.json +22 -0
- sarj_standards/configs/eslint.application.mjs +1366 -0
- sarj_standards/configs/eslint.peers.json +44 -0
- sarj_standards/configs/eslint.strict.mjs +1060 -0
- sarj_standards/configs/markdownlint.strict.yaml +12 -0
- sarj_standards/configs/pyright.strict.json +96 -0
- sarj_standards/configs/ruff.application.toml +363 -0
- sarj_standards/configs/ruff.strict.toml +338 -0
- sarj_standards/configs/rule-inventory.v1.json +1 -0
- sarj_standards/configs/rule-ledger.json +846 -0
- sarj_standards/configs/rule-warning-levels.v1.json +1 -0
- sarj_standards/configs/taplo.strict.toml +14 -0
- sarj_standards/configs/yamllint.strict.yaml +25 -0
- sarj_standards/libs/__init__.py +0 -0
- sarj_standards/libs/adoption/__init__.py +0 -0
- sarj_standards/libs/adoption/configs.py +36 -0
- sarj_standards/libs/adoption/doctor.py +1346 -0
- sarj_standards/libs/adoption/exclusions.py +66 -0
- sarj_standards/libs/adoption/hooks.py +423 -0
- sarj_standards/libs/adoption/launcher.py +240 -0
- sarj_standards/libs/adoption/lifecycle.py +493 -0
- sarj_standards/libs/adoption/manifest.py +550 -0
- sarj_standards/libs/adoption/packagemanager.py +285 -0
- sarj_standards/libs/adoption/retired_suppressions.py +371 -0
- sarj_standards/libs/adoption/scaffold.py +1660 -0
- sarj_standards/libs/adoption/service.py +441 -0
- sarj_standards/libs/adoption/transaction.py +274 -0
- sarj_standards/libs/adoption/upgrade.py +516 -0
- sarj_standards/libs/adoption/uvtool.py +62 -0
- sarj_standards/libs/catalogs/__init__.py +9 -0
- sarj_standards/libs/catalogs/slack_automations.py +627 -0
- sarj_standards/libs/corpus/__init__.py +25 -0
- sarj_standards/libs/corpus/manifest.py +211 -0
- sarj_standards/libs/corpus/snapshot.py +222 -0
- sarj_standards/libs/diagnostics/__init__.py +65 -0
- sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
- sarj_standards/libs/diagnostics/baseline.py +131 -0
- sarj_standards/libs/diagnostics/models.py +574 -0
- sarj_standards/libs/diagnostics/serialize.py +290 -0
- sarj_standards/libs/diagnostics/source.py +172 -0
- sarj_standards/libs/filesystem.py +11 -0
- sarj_standards/libs/linting/__init__.py +0 -0
- sarj_standards/libs/linting/analysis.py +422 -0
- sarj_standards/libs/linting/external.py +1454 -0
- sarj_standards/libs/linting/library_policy.py +688 -0
- sarj_standards/libs/linting/policy.py +152 -0
- sarj_standards/libs/linting/runner.py +442 -0
- sarj_standards/libs/linting/textlint.py +1605 -0
- sarj_standards/libs/release/__init__.py +98 -0
- sarj_standards/libs/release/_values.py +24 -0
- sarj_standards/libs/release/artifacts.py +191 -0
- sarj_standards/libs/release/causality.py +80 -0
- sarj_standards/libs/release/changes.py +48 -0
- sarj_standards/libs/release/process.py +128 -0
- sarj_standards/libs/release/publish.py +85 -0
- sarj_standards/libs/release/registry.py +271 -0
- sarj_standards/libs/release/release_age.py +218 -0
- sarj_standards/libs/release/rollout.py +1163 -0
- sarj_standards/libs/release/tags.py +373 -0
- sarj_standards/libs/release/typescript.py +191 -0
- sarj_standards/libs/repository/__init__.py +0 -0
- sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
- sarj_standards/libs/repository/comment_corpus.py +536 -0
- sarj_standards/libs/repository/config_generation.py +146 -0
- sarj_standards/libs/repository/docs.py +347 -0
- sarj_standards/libs/repository/hooks.py +118 -0
- sarj_standards/libs/repository/ledger.py +99 -0
- sarj_standards/libs/repository/repository.py +744 -0
- sarj_standards/libs/repository/rule_authoring.py +246 -0
- sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
- sarj_standards/libs/repository/rule_changes.py +318 -0
- sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
- sarj_standards/libs/repository/rule_lifecycle.py +167 -0
- sarj_standards/libs/repository/rule_maintenance.py +225 -0
- sarj_standards/libs/rules/__init__.py +74 -0
- sarj_standards/libs/rules/catalog.py +145 -0
- sarj_standards/libs/rules/contracts.py +382 -0
- sarj_standards/libs/rules/corpus_runner.py +365 -0
- sarj_standards/libs/rules/evaluation.py +177 -0
- sarj_standards/libs/setup/__init__.py +4 -0
- sarj_standards/libs/setup/repository.py +40 -0
- sarj_standards/py.typed +0 -0
- sarj_standards/schemas/__init__.py +4 -0
- sarj_standards/schemas/_paths.py +7 -0
- sarj_standards/schemas/rule-catalog.v1.json +1 -0
- sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
- sarj_standards/schemas/slack-automations.v1.schema.json +1751 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess # ruff: ignore[suspicious-subprocess-import] -- fixed local build commands generate the committed catalog.
|
|
9
|
+
from typing import Final, Protocol, TypeGuard
|
|
10
|
+
|
|
11
|
+
from sarj_standards.libs.repository import rule_inventory_artifact
|
|
12
|
+
from sarj_standards.libs.rules import (
|
|
13
|
+
AutofixPolicy,
|
|
14
|
+
DefaultLevel,
|
|
15
|
+
DocumentedRule,
|
|
16
|
+
ExampleFile,
|
|
17
|
+
ExpectedOutcome,
|
|
18
|
+
Language,
|
|
19
|
+
MessageId,
|
|
20
|
+
RuleCatalogDocument,
|
|
21
|
+
RuleCategory,
|
|
22
|
+
RuleEngine,
|
|
23
|
+
RuleExample,
|
|
24
|
+
RuleId,
|
|
25
|
+
RuleSpec,
|
|
26
|
+
)
|
|
27
|
+
from sarj_standards.schemas import RULE_CATALOG
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_CATALOG_PATH: Final = Path("packages/standards/src/sarj_standards/schemas/rule-catalog.v1.json")
|
|
31
|
+
_WARNING_LEVELS_PATH: Final = Path("packages/standards/src/sarj_standards/configs/rule-warning-levels.v1.json")
|
|
32
|
+
_TYPESCRIPT_PACKAGE: Final = Path("packages/typescript")
|
|
33
|
+
_NODE_PROJECTION: Final = (
|
|
34
|
+
"import {publicDocumentation,rules} from './dist/index.js';"
|
|
35
|
+
"process.stdout.write(JSON.stringify(publicDocumentation(rules)));"
|
|
36
|
+
)
|
|
37
|
+
_TYPESCRIPT_FIELDS: Final = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"aliases",
|
|
40
|
+
"autofix",
|
|
41
|
+
"category",
|
|
42
|
+
"code",
|
|
43
|
+
"engine",
|
|
44
|
+
"examples",
|
|
45
|
+
"filePatterns",
|
|
46
|
+
"languages",
|
|
47
|
+
"limitations",
|
|
48
|
+
"messageIds",
|
|
49
|
+
"optionsSchema",
|
|
50
|
+
"rationale",
|
|
51
|
+
"references",
|
|
52
|
+
"remediation",
|
|
53
|
+
"ruleId",
|
|
54
|
+
"since",
|
|
55
|
+
"summary",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
_TYPESCRIPT_EXAMPLE_FIELDS: Final = frozenset(
|
|
59
|
+
{"expectedCount", "files", "fixedFiles", "focusPath", "id", "outcome", "scenarioId", "title"}
|
|
60
|
+
)
|
|
61
|
+
_TYPESCRIPT_FILE_FIELDS: Final = frozenset({"path", "source"})
|
|
62
|
+
_PROCESS_TIMEOUT: Final = timedelta(seconds=120)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class CatalogSyncResult:
|
|
67
|
+
status: int
|
|
68
|
+
message: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load(path: Path = RULE_CATALOG) -> dict[str, object]:
|
|
72
|
+
try:
|
|
73
|
+
payload: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
|
|
74
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
75
|
+
msg = f"cannot load shipped rule catalog {path}: {exc}"
|
|
76
|
+
raise ValueError(msg) from exc
|
|
77
|
+
if not _is_object(payload) or payload.get("schemaVersion") != 1 or not _is_array(payload.get("rules")):
|
|
78
|
+
msg = "shipped rule catalog must contain schemaVersion 1 and a rules array"
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
return payload
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _StringEnum(Protocol):
|
|
84
|
+
@property
|
|
85
|
+
def value(self) -> str: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _NativeExampleFile(Protocol):
|
|
89
|
+
@property
|
|
90
|
+
def path(self) -> PurePosixPath: ...
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def source(self) -> str: ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _NativeExample(Protocol):
|
|
97
|
+
@property
|
|
98
|
+
def example_id(self) -> str: ...
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def title(self) -> str: ...
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def outcome(self) -> _StringEnum: ...
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def files(self) -> tuple[_NativeExampleFile, ...]: ...
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def fixed_files(self) -> tuple[_NativeExampleFile, ...]: ...
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def focus_path(self) -> PurePosixPath: ...
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def expected_count(self) -> int: ...
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def public(self) -> bool: ...
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def scenario(self) -> str: ...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _NativeSpec(Protocol):
|
|
126
|
+
@property
|
|
127
|
+
def rule_id(self) -> str: ...
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def code(self) -> str: ...
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def summary(self) -> str: ...
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def rationale(self) -> str: ...
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def remediation(self) -> str: ...
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def category(self) -> _StringEnum: ...
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def autofix(self) -> _StringEnum: ...
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def aliases(self) -> tuple[str, ...]: ...
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def limitations(self) -> tuple[str, ...]: ...
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def examples(self) -> tuple[_NativeExample, ...]: ...
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _is_object(value: object) -> TypeGuard[dict[str, object]]:
|
|
158
|
+
return isinstance(value, dict) and all(
|
|
159
|
+
isinstance(key, str)
|
|
160
|
+
for key in value # pyright: ignore[reportUnknownVariableType]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_array(value: object) -> TypeGuard[list[object]]:
|
|
165
|
+
return isinstance(value, list)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _native_spec(native: _NativeSpec, *, engine: RuleEngine, languages: frozenset[Language]) -> RuleSpec:
|
|
169
|
+
return RuleSpec(
|
|
170
|
+
engine=engine,
|
|
171
|
+
rule_id=RuleId(native.rule_id),
|
|
172
|
+
code=native.code,
|
|
173
|
+
summary=native.summary,
|
|
174
|
+
rationale=native.rationale,
|
|
175
|
+
remediation=native.remediation,
|
|
176
|
+
category=RuleCategory(native.category.value),
|
|
177
|
+
languages=languages,
|
|
178
|
+
autofix=AutofixPolicy(native.autofix.value),
|
|
179
|
+
aliases=native.aliases,
|
|
180
|
+
limitations=native.limitations,
|
|
181
|
+
examples=tuple(_example(item) for item in native.examples),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _example(native: _NativeExample) -> RuleExample:
|
|
186
|
+
return RuleExample(
|
|
187
|
+
example_id=native.example_id,
|
|
188
|
+
title=native.title,
|
|
189
|
+
outcome=ExpectedOutcome(native.outcome.value),
|
|
190
|
+
files=tuple(_example_file(item) for item in native.files),
|
|
191
|
+
fixed_files=tuple(_example_file(item) for item in native.fixed_files),
|
|
192
|
+
focus_path=native.focus_path,
|
|
193
|
+
expected_count=native.expected_count,
|
|
194
|
+
public=native.public,
|
|
195
|
+
scenario=native.scenario,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _example_file(native: _NativeExampleFile) -> ExampleFile:
|
|
200
|
+
return ExampleFile(path=native.path, source=native.source)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def parse_typescript_projection(payload: object) -> tuple[RuleSpec, ...]:
|
|
204
|
+
if not _is_array(payload):
|
|
205
|
+
msg = "TypeScript public metadata projection must be an array"
|
|
206
|
+
raise TypeError(msg)
|
|
207
|
+
return tuple(_typescript_spec(item) for item in payload)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _typescript_spec(value: object) -> RuleSpec:
|
|
211
|
+
if not _is_object(value):
|
|
212
|
+
msg = "TypeScript public metadata entry must be an object"
|
|
213
|
+
raise TypeError(msg)
|
|
214
|
+
if frozenset(value) != _TYPESCRIPT_FIELDS:
|
|
215
|
+
msg = "TypeScript public metadata entry has unexpected or missing fields"
|
|
216
|
+
raise ValueError(msg)
|
|
217
|
+
if value["engine"] != RuleEngine.ESLINT.value or value["code"] is not None:
|
|
218
|
+
msg = "TypeScript public metadata has invalid engine or code"
|
|
219
|
+
raise ValueError(msg)
|
|
220
|
+
examples = value.get("examples")
|
|
221
|
+
if not _is_array(examples):
|
|
222
|
+
msg = "TypeScript public metadata examples must be an array"
|
|
223
|
+
raise TypeError(msg)
|
|
224
|
+
options = value.get("optionsSchema")
|
|
225
|
+
if options is not None and not _is_object(options):
|
|
226
|
+
msg = "TypeScript public metadata optionsSchema must be an object or null"
|
|
227
|
+
raise TypeError(msg)
|
|
228
|
+
since = value.get("since")
|
|
229
|
+
if since is not None and not isinstance(since, str):
|
|
230
|
+
msg = "TypeScript public metadata since must be a string or null"
|
|
231
|
+
raise TypeError(msg)
|
|
232
|
+
return RuleSpec(
|
|
233
|
+
engine=RuleEngine.ESLINT,
|
|
234
|
+
rule_id=RuleId(_value_from_dict(value, "ruleId")),
|
|
235
|
+
code=None,
|
|
236
|
+
summary=_value_from_dict(value, "summary"),
|
|
237
|
+
rationale=_value_from_dict(value, "rationale"),
|
|
238
|
+
remediation=_value_from_dict(value, "remediation"),
|
|
239
|
+
category=RuleCategory(_value_from_dict(value, "category")),
|
|
240
|
+
languages=frozenset(Language(item) for item in _string_list(value, "languages")),
|
|
241
|
+
autofix=AutofixPolicy(_value_from_dict(value, "autofix")),
|
|
242
|
+
aliases=tuple(_string_list(value, "aliases")),
|
|
243
|
+
limitations=tuple(_string_list(value, "limitations")),
|
|
244
|
+
file_patterns=tuple(_string_list(value, "filePatterns")),
|
|
245
|
+
message_ids=tuple(MessageId(item) for item in _string_list(value, "messageIds")),
|
|
246
|
+
options_schema=None if options is None else json.dumps(options, separators=(",", ":"), sort_keys=True),
|
|
247
|
+
references=tuple(_string_list(value, "references")),
|
|
248
|
+
since=since,
|
|
249
|
+
examples=tuple(_typescript_example(item) for item in examples),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _value_from_dict(value: dict[str, object], name: str) -> str:
|
|
254
|
+
item = value.get(name)
|
|
255
|
+
if not isinstance(item, str) or not item:
|
|
256
|
+
msg = f"TypeScript public metadata has invalid {name}"
|
|
257
|
+
raise ValueError(msg)
|
|
258
|
+
return item
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _string_list(value: dict[str, object], name: str) -> list[str]:
|
|
262
|
+
item = value.get(name)
|
|
263
|
+
if not _is_array(item) or not all(isinstance(entry, str) for entry in item):
|
|
264
|
+
msg = f"TypeScript public metadata has invalid {name}"
|
|
265
|
+
raise ValueError(msg)
|
|
266
|
+
return [entry for entry in item if isinstance(entry, str)]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _typescript_example(value: object) -> RuleExample:
|
|
270
|
+
if not _is_object(value):
|
|
271
|
+
msg = "TypeScript public example must be an object"
|
|
272
|
+
raise TypeError(msg)
|
|
273
|
+
if frozenset(value) != _TYPESCRIPT_EXAMPLE_FIELDS:
|
|
274
|
+
msg = "TypeScript public example has unexpected or missing fields"
|
|
275
|
+
raise ValueError(msg)
|
|
276
|
+
files = value.get("files")
|
|
277
|
+
fixed_files = value.get("fixedFiles")
|
|
278
|
+
if not _is_array(files) or not _is_array(fixed_files):
|
|
279
|
+
msg = "TypeScript public example files must be arrays"
|
|
280
|
+
raise TypeError(msg)
|
|
281
|
+
focus_path = _value_from_dict(value, "focusPath")
|
|
282
|
+
expected_count = value.get("expectedCount")
|
|
283
|
+
if type(expected_count) is not int:
|
|
284
|
+
msg = "TypeScript public example expectedCount must be an integer"
|
|
285
|
+
raise TypeError(msg)
|
|
286
|
+
return RuleExample(
|
|
287
|
+
example_id=_value_from_dict(value, "id"),
|
|
288
|
+
title=_value_from_dict(value, "title"),
|
|
289
|
+
outcome=ExpectedOutcome(_value_from_dict(value, "outcome")),
|
|
290
|
+
files=tuple(_typescript_file(item) for item in files),
|
|
291
|
+
fixed_files=tuple(_typescript_file(item) for item in fixed_files),
|
|
292
|
+
focus_path=PurePosixPath(focus_path),
|
|
293
|
+
expected_count=expected_count,
|
|
294
|
+
public=True,
|
|
295
|
+
scenario=_value_from_dict(value, "scenarioId"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _typescript_file(value: object) -> ExampleFile:
|
|
300
|
+
if not _is_object(value):
|
|
301
|
+
msg = "TypeScript public example file must be an object"
|
|
302
|
+
raise TypeError(msg)
|
|
303
|
+
if frozenset(value) != _TYPESCRIPT_FILE_FIELDS:
|
|
304
|
+
msg = "TypeScript public example file has unexpected or missing fields"
|
|
305
|
+
raise ValueError(msg)
|
|
306
|
+
return ExampleFile(
|
|
307
|
+
path=PurePosixPath(_value_from_dict(value, "path")),
|
|
308
|
+
source=_value_from_dict(value, "source"),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def build( # ruff: ignore[too-many-locals] -- joins five engine registries with lifecycle metadata.
|
|
313
|
+
root: Path,
|
|
314
|
+
) -> RuleCatalogDocument:
|
|
315
|
+
from sarj_standards.libs.linting import textlint # ruff: ignore[import-outside-top-level]
|
|
316
|
+
|
|
317
|
+
resolved = root.resolve()
|
|
318
|
+
warning_rules = _warning_rules(resolved)
|
|
319
|
+
inventory = rule_inventory_artifact.build(resolved)
|
|
320
|
+
raw_rules = inventory["rules"]
|
|
321
|
+
if not _is_array(raw_rules):
|
|
322
|
+
msg = "rule inventory rules must be an array"
|
|
323
|
+
raise TypeError(msg)
|
|
324
|
+
locations: dict[tuple[str, str], tuple[PurePosixPath, PurePosixPath]] = {}
|
|
325
|
+
for value in raw_rules:
|
|
326
|
+
if not _is_object(value):
|
|
327
|
+
msg = "rule inventory entry must be an object"
|
|
328
|
+
raise TypeError(msg)
|
|
329
|
+
family = _value_from_dict(value, "family")
|
|
330
|
+
rule_id = _value_from_dict(value, "id")
|
|
331
|
+
locations[family, rule_id] = (
|
|
332
|
+
PurePosixPath(_value_from_dict(value, "source")),
|
|
333
|
+
PurePosixPath(_value_from_dict(value, "test")),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
specs = (
|
|
337
|
+
*_python_specs(),
|
|
338
|
+
*_sql_specs(),
|
|
339
|
+
*_iac_specs(),
|
|
340
|
+
*(meta.native_spec(rule_id) for rule_id, meta in textlint.REGISTRY.items()),
|
|
341
|
+
*_typescript_specs(resolved),
|
|
342
|
+
)
|
|
343
|
+
engine_family = {
|
|
344
|
+
RuleEngine.ESLINT: "typescript",
|
|
345
|
+
RuleEngine.IAC: "iac",
|
|
346
|
+
RuleEngine.PYTHON: "python",
|
|
347
|
+
RuleEngine.SQL: "sql",
|
|
348
|
+
RuleEngine.TEXT: "text",
|
|
349
|
+
}
|
|
350
|
+
documented: list[DocumentedRule] = []
|
|
351
|
+
for spec in specs:
|
|
352
|
+
family = engine_family[spec.engine]
|
|
353
|
+
try:
|
|
354
|
+
source, test = locations.pop((family, spec.rule_id))
|
|
355
|
+
except KeyError as exc:
|
|
356
|
+
msg = f"{spec.key} is documented but absent from the live inventory"
|
|
357
|
+
raise ValueError(msg) from exc
|
|
358
|
+
documented.append(
|
|
359
|
+
DocumentedRule(
|
|
360
|
+
spec=spec,
|
|
361
|
+
default_level=(DefaultLevel.WARNING if spec.key in warning_rules else DefaultLevel.ERROR),
|
|
362
|
+
source=source,
|
|
363
|
+
test=test,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
if locations:
|
|
367
|
+
missing = ", ".join(f"{family}:{rule_id}" for family, rule_id in sorted(locations))
|
|
368
|
+
msg = f"live rules missing source-owned documentation: {missing}"
|
|
369
|
+
raise ValueError(msg)
|
|
370
|
+
live_keys = {rule.spec.key for rule in documented}
|
|
371
|
+
unknown_warning_rules = warning_rules - live_keys
|
|
372
|
+
if unknown_warning_rules:
|
|
373
|
+
msg = f"warning lifecycle names unknown rules: {', '.join(sorted(unknown_warning_rules))}"
|
|
374
|
+
raise ValueError(msg)
|
|
375
|
+
return RuleCatalogDocument(tuple(documented))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _warning_rules(root: Path) -> frozenset[str]:
|
|
379
|
+
path = root / _WARNING_LEVELS_PATH
|
|
380
|
+
payload: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
|
|
381
|
+
if not _is_object(payload) or set(payload) != {"rules", "schemaVersion"}:
|
|
382
|
+
msg = "rule warning lifecycle must contain exactly rules and schemaVersion"
|
|
383
|
+
raise ValueError(msg)
|
|
384
|
+
if payload["schemaVersion"] != 1 or not _is_array(payload["rules"]):
|
|
385
|
+
msg = "rule warning lifecycle must use schemaVersion 1 and an array of rules"
|
|
386
|
+
raise TypeError(msg)
|
|
387
|
+
rules = payload["rules"]
|
|
388
|
+
if any(not isinstance(value, str) for value in rules):
|
|
389
|
+
msg = "rule warning lifecycle selectors must be strings"
|
|
390
|
+
raise TypeError(msg)
|
|
391
|
+
selected = frozenset(value for value in rules if isinstance(value, str))
|
|
392
|
+
if len(selected) != len(rules):
|
|
393
|
+
msg = "rule warning lifecycle repeats a selector"
|
|
394
|
+
raise ValueError(msg)
|
|
395
|
+
return selected
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _python_specs() -> tuple[RuleSpec, ...]:
|
|
399
|
+
from sarj_python_lint.rules import REGISTRY # ruff: ignore[import-outside-top-level]
|
|
400
|
+
|
|
401
|
+
specs: list[RuleSpec] = []
|
|
402
|
+
for rule_id, rule in REGISTRY.items():
|
|
403
|
+
native = rule.native_spec()
|
|
404
|
+
if native is None:
|
|
405
|
+
msg = f"python:{rule_id} is missing source-owned documentation"
|
|
406
|
+
raise ValueError(msg)
|
|
407
|
+
specs.append(_native_spec(native, engine=RuleEngine.PYTHON, languages=frozenset({Language.PYTHON})))
|
|
408
|
+
return tuple(specs)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _sql_specs() -> tuple[RuleSpec, ...]:
|
|
412
|
+
from sarj_sql_lint.rules import REGISTRY # ruff: ignore[import-outside-top-level]
|
|
413
|
+
|
|
414
|
+
specs: list[RuleSpec] = []
|
|
415
|
+
for rule_id, rule in REGISTRY.items():
|
|
416
|
+
native = rule.native_spec()
|
|
417
|
+
if native is None:
|
|
418
|
+
msg = f"sql:{rule_id} is missing source-owned documentation"
|
|
419
|
+
raise ValueError(msg)
|
|
420
|
+
specs.append(_native_spec(native, engine=RuleEngine.SQL, languages=frozenset({Language.SQL})))
|
|
421
|
+
return tuple(specs)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _iac_specs() -> tuple[RuleSpec, ...]:
|
|
425
|
+
from sarj_iac_lint.rules import REGISTRY # ruff: ignore[import-outside-top-level]
|
|
426
|
+
|
|
427
|
+
specs: list[RuleSpec] = []
|
|
428
|
+
for rule_id, rule in REGISTRY.items():
|
|
429
|
+
native = rule.native_spec()
|
|
430
|
+
if native is None:
|
|
431
|
+
msg = f"iac:{rule_id} is missing source-owned documentation"
|
|
432
|
+
raise ValueError(msg)
|
|
433
|
+
specs.append(_native_spec(native, engine=RuleEngine.IAC, languages=frozenset({Language.IAC})))
|
|
434
|
+
return tuple(specs)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _typescript_specs(root: Path) -> tuple[RuleSpec, ...]:
|
|
438
|
+
package = root / _TYPESCRIPT_PACKAGE
|
|
439
|
+
npm = shutil.which("npm")
|
|
440
|
+
node = shutil.which("node")
|
|
441
|
+
if npm is None or node is None:
|
|
442
|
+
missing = "npm" if npm is None else "node"
|
|
443
|
+
msg = f"cannot generate the TypeScript rule catalog: {missing} is not installed"
|
|
444
|
+
raise RuntimeError(msg)
|
|
445
|
+
subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- resolved executable and fixed argv.
|
|
446
|
+
(npm, "run", "build", "--silent"),
|
|
447
|
+
cwd=package,
|
|
448
|
+
check=True,
|
|
449
|
+
timeout=_PROCESS_TIMEOUT.total_seconds(),
|
|
450
|
+
)
|
|
451
|
+
completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- resolved executable and fixed projection.
|
|
452
|
+
(node, "--input-type=module", "--eval", _NODE_PROJECTION),
|
|
453
|
+
cwd=package,
|
|
454
|
+
check=True,
|
|
455
|
+
capture_output=True,
|
|
456
|
+
text=True,
|
|
457
|
+
timeout=_PROCESS_TIMEOUT.total_seconds(),
|
|
458
|
+
)
|
|
459
|
+
payload: object = json.loads(completed.stdout) # pyright: ignore[reportAny]
|
|
460
|
+
return parse_typescript_projection(payload)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def render(root: Path) -> str:
|
|
464
|
+
return json.dumps(build(root).as_public_dict(), ensure_ascii=False, separators=(",", ":"), sort_keys=True) + "\n"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def sync(root: Path, *, check: bool) -> CatalogSyncResult:
|
|
468
|
+
from sarj_standards.libs.adoption import transaction # ruff: ignore[import-outside-top-level]
|
|
469
|
+
|
|
470
|
+
resolved = root.resolve()
|
|
471
|
+
destination = resolved / _CATALOG_PATH
|
|
472
|
+
expected = render(resolved)
|
|
473
|
+
current = destination.read_text(encoding="utf-8") if destination.is_file() else ""
|
|
474
|
+
if current == expected:
|
|
475
|
+
return CatalogSyncResult(0, "ok: rule-catalog.v1.json matches source-owned metadata")
|
|
476
|
+
if check:
|
|
477
|
+
return CatalogSyncResult(1, "drift: rule-catalog.v1.json differs; run `code-standards maintain catalog sync`")
|
|
478
|
+
transaction.atomic_write_text(resolved, destination, expected)
|
|
479
|
+
return CatalogSyncResult(0, "updated: rule-catalog.v1.json")
|