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,274 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ from pathlib import Path
6
+ import stat
7
+ import tempfile
8
+ from typing import Final, Self
9
+
10
+ from sarj_standards.libs.filesystem import is_link_like
11
+
12
+
13
+ _OWNED_NAMES: Final = frozenset(
14
+ {
15
+ ".pre-commit-config.yaml",
16
+ ".pre-commit-config.yml",
17
+ ".sarj-standards.toml",
18
+ ".ruff-strict.toml",
19
+ ".pyright-strict.json",
20
+ ".markdownlint.yaml",
21
+ ".taplo.toml",
22
+ ".yamllint.yaml",
23
+ "eslint.config.mjs",
24
+ "eslint.strict.mjs",
25
+ "package.json",
26
+ "package-lock.json",
27
+ "pnpm-lock.yaml",
28
+ "yarn.lock",
29
+ "bun.lock",
30
+ "bun.lockb",
31
+ "pyproject.toml",
32
+ "pyrightconfig.json",
33
+ "pyrightconfig.jsonc",
34
+ "uv.lock",
35
+ }
36
+ )
37
+ _SKIP_DIRS: Final = frozenset({".git", ".venv", "node_modules", "dist", "build", ".next", ".cache"})
38
+ _INSTALL_MUTATION_NAMES: Final = frozenset(
39
+ {
40
+ "package.json",
41
+ "package-lock.json",
42
+ "pnpm-lock.yaml",
43
+ "yarn.lock",
44
+ "bun.lock",
45
+ "bun.lockb",
46
+ "pyproject.toml",
47
+ "uv.lock",
48
+ }
49
+ )
50
+
51
+
52
+ def validate_targets(root: Path, paths: tuple[Path, ...]) -> None:
53
+ resolved_root = root.resolve()
54
+ lexical_root = root.absolute()
55
+ for path in paths:
56
+ try:
57
+ relative = path.absolute().relative_to(lexical_root)
58
+ except ValueError as exc:
59
+ msg = f"mutation target {path} escapes repository root {resolved_root}"
60
+ raise OSError(msg) from exc
61
+ current = lexical_root
62
+ for part in relative.parts:
63
+ current /= part
64
+ if is_link_like(current):
65
+ msg = f"refusing symlink mutation target {path}; link traversal at {current}"
66
+ raise OSError(msg)
67
+ if path.exists() and not path.is_file():
68
+ msg = f"refusing non-file mutation target {path}"
69
+ raise OSError(msg)
70
+ if path.is_file() and path.stat(follow_symlinks=False).st_nlink > 1:
71
+ msg = f"refusing hard-linked mutation target {path}"
72
+ raise OSError(msg)
73
+ try:
74
+ path.resolve().relative_to(resolved_root)
75
+ except (OSError, ValueError) as exc:
76
+ msg = f"mutation target {path} escapes repository root {resolved_root}"
77
+ raise OSError(msg) from exc
78
+
79
+
80
+ def atomic_write_text(root: Path, path: Path, contents: str) -> None:
81
+ atomic_write_bytes(root, path, contents.encode("utf-8"))
82
+
83
+
84
+ def atomic_write_bytes(root: Path, path: Path, contents: bytes, *, mode: int | None = None) -> None:
85
+ validate_targets(root, (path,))
86
+ path.parent.mkdir(parents=True, exist_ok=True)
87
+ validate_targets(root, (path,))
88
+ if mode is None:
89
+ mode = stat.S_IMODE(path.stat(follow_symlinks=False).st_mode) if path.is_file() else 0o644
90
+ descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
91
+ temporary_path = Path(temporary)
92
+ try:
93
+ _write_temporary(descriptor, contents, mode)
94
+ temporary_path.replace(path)
95
+ except BaseException:
96
+ temporary_path.unlink(missing_ok=True)
97
+ raise
98
+
99
+
100
+ def assert_expected(root: Path, path: Path, expected: bytes | None) -> None:
101
+ validate_targets(root, (path,))
102
+ current = path.read_bytes() if path.is_file() else None
103
+ if current != expected:
104
+ msg = f"planned mutation target changed concurrently: {path}; rerun the command"
105
+ raise OSError(msg)
106
+
107
+
108
+ def remove_file(root: Path, path: Path) -> None:
109
+ validate_targets(root, (path,))
110
+ try:
111
+ path.unlink()
112
+ except FileNotFoundError as exc:
113
+ msg = f"planned deletion target changed concurrently: {path}; rerun the command"
114
+ raise OSError(msg) from exc
115
+
116
+
117
+ def _write_temporary(descriptor: int, contents: bytes, mode: int) -> None:
118
+ with os.fdopen(descriptor, "wb") as handle:
119
+ os.fchmod(handle.fileno(), mode)
120
+ _ = handle.write(contents)
121
+ handle.flush()
122
+ os.fsync(handle.fileno())
123
+
124
+
125
+ @dataclass
126
+ class FileSnapshot:
127
+ contents: bytes | None
128
+ mode: int | None
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class RollbackIssue:
133
+ path: Path
134
+ detail: str
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class RollbackReport:
139
+ issues: tuple[RollbackIssue, ...] = ()
140
+
141
+ @property
142
+ def ok(self) -> bool:
143
+ return not self.issues
144
+
145
+ def render(self) -> str | None:
146
+ if not self.issues:
147
+ return None
148
+ return "; ".join(f"could not restore {issue.path}: {issue.detail}" for issue in self.issues)
149
+
150
+
151
+ @dataclass
152
+ class FileTransaction:
153
+ root: Path
154
+ before: dict[Path, FileSnapshot]
155
+ written: dict[Path, bytes | None]
156
+ absent_parents: set[Path]
157
+
158
+ @classmethod
159
+ def capture(cls, root: Path, extra: tuple[Path, ...] = ()) -> Self:
160
+ resolved = root.resolve()
161
+ candidates = set(extra)
162
+ for parent, directories, names in os.walk(resolved):
163
+ directories[:] = [name for name in directories if name not in _SKIP_DIRS]
164
+ directory = Path(parent)
165
+ candidates.update(directory / name for name in names if name in _OWNED_NAMES)
166
+ if "pyproject.toml" in names:
167
+ candidates.add(directory / "uv.lock")
168
+ if "package.json" in names:
169
+ candidates.update(
170
+ directory / name
171
+ for name in ("package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb")
172
+ )
173
+ before: dict[Path, FileSnapshot] = {}
174
+ absent_parents: set[Path] = set()
175
+ for path in candidates:
176
+ if is_link_like(path):
177
+ if path.name in _INSTALL_MUTATION_NAMES:
178
+ msg = f"refusing linked dependency transaction target {path}"
179
+ raise OSError(msg)
180
+ continue
181
+ try:
182
+ path.resolve().relative_to(resolved)
183
+ except OSError, ValueError:
184
+ continue
185
+ if path.is_file() and path.stat(follow_symlinks=False).st_nlink > 1:
186
+ msg = f"refusing hard-linked transaction target {path}"
187
+ raise OSError(msg)
188
+ before[path] = _snapshot(path)
189
+ absent_parents.update(_absent_parents(resolved, path.parent))
190
+ return cls(resolved, before, {}, absent_parents)
191
+
192
+ def track(self, *paths: Path) -> None:
193
+ for path in paths:
194
+ if is_link_like(path):
195
+ continue
196
+ if path not in self.before:
197
+ self.before[path] = _snapshot(path)
198
+ self.absent_parents.update(_absent_parents(self.root, path.parent))
199
+
200
+ def mark_written(self, *paths: Path) -> None:
201
+ for path in paths:
202
+ self.written[path] = path.read_bytes() if path.is_file() else None
203
+
204
+ def rollback(self) -> RollbackReport:
205
+ issues: list[RollbackIssue] = []
206
+ for path, snapshot in self.before.items():
207
+ expected = self.written.get(path, _UNTRACKED)
208
+ issue = _restore_path(self.root, path, snapshot, expected)
209
+ if issue is not None:
210
+ issues.append(issue)
211
+ for parent in sorted(self.absent_parents, key=lambda item: len(item.parts), reverse=True):
212
+ try:
213
+ parent.rmdir()
214
+ except FileNotFoundError:
215
+ continue
216
+ except OSError:
217
+ # A non-empty directory contains state the transaction does not own.
218
+ continue
219
+ return RollbackReport(tuple(issues))
220
+
221
+
222
+ class _Untracked:
223
+ """Sentinel type for paths without a recorded standards write."""
224
+
225
+
226
+ _UNTRACKED: Final = _Untracked()
227
+
228
+
229
+ def _snapshot(path: Path) -> FileSnapshot:
230
+ if not path.is_file():
231
+ return FileSnapshot(None, None)
232
+ metadata = path.stat(follow_symlinks=False)
233
+ return FileSnapshot(path.read_bytes(), stat.S_IMODE(metadata.st_mode))
234
+
235
+
236
+ def _restore_path(
237
+ root: Path,
238
+ path: Path,
239
+ snapshot: FileSnapshot,
240
+ expected: bytes | _Untracked | None,
241
+ ) -> RollbackIssue | None:
242
+ try:
243
+ validate_targets(root, (path,))
244
+ current = path.read_bytes() if path.is_file() else None
245
+ except OSError as exc:
246
+ return RollbackIssue(path, str(exc))
247
+ if not isinstance(expected, _Untracked) and current != expected:
248
+ return RollbackIssue(path, "changed concurrently after the standards write")
249
+ try:
250
+ _apply_snapshot(root, path, snapshot)
251
+ except OSError as exc:
252
+ return RollbackIssue(path, str(exc))
253
+ return None
254
+
255
+
256
+ def _apply_snapshot(root: Path, path: Path, snapshot: FileSnapshot) -> None:
257
+ if snapshot.contents is None:
258
+ if path.is_file() or is_link_like(path):
259
+ path.unlink()
260
+ return
261
+ atomic_write_bytes(root, path, snapshot.contents)
262
+ if snapshot.mode is not None:
263
+ path.chmod(snapshot.mode)
264
+
265
+
266
+ def _absent_parents(root: Path, parent: Path) -> set[Path]:
267
+ missing: set[Path] = set()
268
+ current = parent
269
+ while current != root and current.is_relative_to(root):
270
+ if current.exists():
271
+ break
272
+ missing.add(current)
273
+ current = current.parent
274
+ return missing