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,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")