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