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,211 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ import os
6
+ from pathlib import Path
7
+ import re
8
+ import stat
9
+ import tomllib
10
+ from typing import TypeIs
11
+
12
+ from sarj_standards.libs.filesystem import is_link_like
13
+
14
+
15
+ _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
16
+ _REVISION = re.compile(r"^[0-9a-f]{40}$")
17
+
18
+
19
+ class CorpusKind(StrEnum):
20
+ LOCAL = "local"
21
+ GIT = "git"
22
+
23
+
24
+ class CorpusVisibility(StrEnum):
25
+ PUBLIC = "public"
26
+ PRIVATE = "private"
27
+
28
+
29
+ @dataclass(frozen=True, slots=True, repr=False)
30
+ class CorpusSource:
31
+ name: str = field(repr=False)
32
+ root: Path = field(repr=False)
33
+ kind: CorpusKind
34
+ digest: str
35
+ include: tuple[str, ...]
36
+ exclude: tuple[str, ...] = ()
37
+ revision: str | None = None
38
+ visibility: CorpusVisibility = CorpusVisibility.PUBLIC
39
+
40
+ def __repr__(self) -> str:
41
+ if self.visibility is CorpusVisibility.PRIVATE:
42
+ return f"CorpusSource(name='<private-corpus>', kind={self.kind!r}, visibility={self.visibility!r})"
43
+ return (
44
+ f"CorpusSource(name={self.name!r}, root={self.root!r}, kind={self.kind!r}, "
45
+ f"digest={self.digest!r}, include={self.include!r}, exclude={self.exclude!r}, "
46
+ f"revision={self.revision!r}, visibility={self.visibility!r})"
47
+ )
48
+
49
+ def __post_init__(self) -> None:
50
+ if not self.name or not self.name.replace("-", "").isalnum() or self.name != self.name.lower():
51
+ msg = "corpus name must be non-empty lowercase kebab-case"
52
+ raise ValueError(msg)
53
+ if not _DIGEST.fullmatch(self.digest):
54
+ msg = f"corpus {self.report_name} requires a sha256 content digest"
55
+ raise ValueError(msg)
56
+ if not self.include or any(not pattern.strip() for pattern in (*self.include, *self.exclude)):
57
+ msg = f"corpus {self.report_name} requires non-empty include patterns"
58
+ raise ValueError(msg)
59
+ if self.kind is CorpusKind.GIT and (self.revision is None or not _REVISION.fullmatch(self.revision)):
60
+ msg = f"git corpus {self.report_name} requires a full lowercase 40-character revision"
61
+ raise ValueError(msg)
62
+ if self.kind is CorpusKind.LOCAL and self.revision is not None:
63
+ msg = f"local corpus {self.report_name} cannot declare a Git revision"
64
+ raise ValueError(msg)
65
+
66
+ @property
67
+ def report_name(self) -> str:
68
+ return self.name if self.visibility is CorpusVisibility.PUBLIC else "<private-corpus>"
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class CorpusManifest:
73
+ origin: Path = field(repr=False)
74
+ sources: tuple[CorpusSource, ...]
75
+ private: bool = False
76
+ schema: int = 1
77
+
78
+ def __post_init__(self) -> None:
79
+ if self.schema != 1:
80
+ msg = f"unsupported corpus manifest schema: {self.schema}"
81
+ raise ValueError(msg)
82
+ names = [source.name for source in self.sources]
83
+ if len(names) != len(set(names)):
84
+ msg = "corpus names must be unique"
85
+ raise ValueError(msg)
86
+
87
+
88
+ def _is_object_list(value: object) -> TypeIs[list[object]]:
89
+ return isinstance(value, list)
90
+
91
+
92
+ def load_manifest(path: Path) -> CorpusManifest:
93
+ return _load(path, expect_private=False)
94
+
95
+
96
+ def load_private_overlay(path: Path) -> CorpusManifest:
97
+ if os.name == "nt":
98
+ msg = "private corpus overlays require POSIX owner-only permission semantics"
99
+ raise OSError(msg)
100
+ if is_link_like(path) or not path.is_file():
101
+ msg = "private corpus overlay must be a regular non-symlink file"
102
+ raise ValueError(msg)
103
+ mode = stat.S_IMODE(path.stat().st_mode)
104
+ if mode & 0o077:
105
+ msg = "private corpus overlay must be owner-readable only (chmod 600)"
106
+ raise PermissionError(msg)
107
+ return _load(path, expect_private=True)
108
+
109
+
110
+ def _load(path: Path, *, expect_private: bool) -> CorpusManifest:
111
+ try:
112
+ parsed: object = tomllib.loads(path.read_text(encoding="utf-8"))
113
+ except (OSError, tomllib.TOMLDecodeError) as exc:
114
+ label = "<private-corpus-overlay>" if expect_private else str(path)
115
+ msg = f"could not read corpus manifest {label}"
116
+ if not expect_private:
117
+ msg = f"{msg}: {exc}"
118
+ raise ValueError(msg) from (None if expect_private else exc)
119
+ root = _table(parsed, label="corpus manifest")
120
+ schema = root.get("schema", 1)
121
+ private = root.get("private", False)
122
+ if not isinstance(schema, int) or not isinstance(private, bool):
123
+ msg = "corpus manifest schema/private fields have invalid types"
124
+ raise TypeError(msg)
125
+ if private is not expect_private:
126
+ expected = "private overlay" if expect_private else "public manifest"
127
+ msg = f"expected a {expected}"
128
+ raise ValueError(msg)
129
+ raw_sources = root.get("corpus", [])
130
+ if not _is_object_list(raw_sources):
131
+ msg = "corpus must be an array of tables"
132
+ raise TypeError(msg)
133
+ visibility = CorpusVisibility.PRIVATE if private else CorpusVisibility.PUBLIC
134
+ sources = tuple(_source(_table(item, label="corpus entry"), path, visibility) for item in raw_sources)
135
+ return CorpusManifest(path.resolve(), sources, private, schema)
136
+
137
+
138
+ def _table(value: object, *, label: str) -> dict[str, object]:
139
+ if not _is_object_dict(value):
140
+ msg = f"{label} must be a table"
141
+ raise TypeError(msg)
142
+ table: dict[str, object] = {}
143
+ for key, item in value.items():
144
+ if not isinstance(key, str):
145
+ msg = f"{label} contains a non-string key"
146
+ raise TypeError(msg)
147
+ table[key] = item
148
+ return table
149
+
150
+
151
+ def _is_object_dict(value: object) -> TypeIs[dict[object, object]]:
152
+ return isinstance(value, dict)
153
+
154
+
155
+ def _source(values: dict[str, object], origin: Path, visibility: CorpusVisibility) -> CorpusSource:
156
+ required = {"name", "root", "kind", "digest", "include"}
157
+ missing = sorted(required - values.keys())
158
+ if missing:
159
+ msg = f"corpus entry is missing: {', '.join(missing)}"
160
+ raise ValueError(msg)
161
+ name, root, kind, digest = (values[key] for key in ("name", "root", "kind", "digest"))
162
+ revision = values.get("revision")
163
+ if (
164
+ not isinstance(name, str)
165
+ or not isinstance(root, str)
166
+ or not isinstance(kind, str)
167
+ or not isinstance(digest, str)
168
+ ):
169
+ msg = "corpus name/root/kind/digest must be strings"
170
+ raise TypeError(msg)
171
+ if revision is not None and not isinstance(revision, str):
172
+ msg = "corpus revision must be a string"
173
+ raise TypeError(msg)
174
+ resolved = Path(root)
175
+ if visibility is CorpusVisibility.PUBLIC and resolved.is_absolute():
176
+ msg = "public corpus roots must be relative to the manifest"
177
+ raise ValueError(msg)
178
+ if not resolved.is_absolute():
179
+ if visibility is CorpusVisibility.PUBLIC and ".." in resolved.parts:
180
+ msg = "public corpus roots must stay below the manifest directory"
181
+ raise ValueError(msg)
182
+ resolved = (origin.parent / resolved).resolve()
183
+ return CorpusSource(
184
+ name,
185
+ resolved,
186
+ CorpusKind(kind),
187
+ digest,
188
+ _strings(values["include"], label="corpus include"),
189
+ _strings(values.get("exclude", []), label="corpus exclude"),
190
+ revision,
191
+ visibility,
192
+ )
193
+
194
+
195
+ def _strings(value: object, *, label: str) -> tuple[str, ...]:
196
+ if not _is_object_list(value) or not all(isinstance(item, str) for item in value):
197
+ msg = f"{label} must be an array of strings"
198
+ raise TypeError(msg)
199
+ return tuple(item for item in value if isinstance(item, str))
200
+
201
+
202
+ def merge_manifests(public: CorpusManifest, private: CorpusManifest | None = None) -> CorpusManifest:
203
+ if public.private:
204
+ msg = "base corpus manifest must be public"
205
+ raise ValueError(msg)
206
+ if private is None:
207
+ return public
208
+ if not private.private:
209
+ msg = "corpus overlay must be private"
210
+ raise ValueError(msg)
211
+ return CorpusManifest(public.origin, (*public.sources, *private.sources))
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from fnmatch import fnmatch
5
+ import hashlib
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import shutil
10
+ import subprocess # ruff: ignore[suspicious-subprocess-import] -- local, fixed-argument Git verification only.
11
+ from typing import Final, NamedTuple
12
+
13
+ from sarj_standards.libs.filesystem import is_link_like
14
+
15
+ from .manifest import CorpusKind, CorpusSource
16
+
17
+
18
+ _GIT_REVISION_OUTPUT_LINES = 2
19
+ _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
20
+ _REVISION = re.compile(r"^[0-9a-f]{40}$")
21
+ _GIT_SAFE_ENV: Final = frozenset(
22
+ {"HOME", "LANG", "LC_ALL", "LC_CTYPE", "PATH", "SYSTEMDRIVE", "SYSTEMROOT", "TMPDIR", "XDG_CONFIG_HOME"}
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True, slots=True, repr=False)
27
+ class CorpusSnapshot:
28
+ name: str
29
+ digest: str
30
+ files: int
31
+ bytes: int
32
+ revision: str | None = None
33
+ private: bool = field(default=False, repr=False, compare=False)
34
+ verified: bool = field(default=False, init=False, repr=False, compare=False)
35
+
36
+ def __post_init__(self) -> None:
37
+ if not self.name or not _DIGEST.fullmatch(self.digest):
38
+ msg = "corpus snapshot requires a name and sha256 digest"
39
+ raise ValueError(msg)
40
+ if self.files <= 0 or self.bytes < 0:
41
+ msg = "corpus snapshot counts must contain files and non-negative bytes"
42
+ raise ValueError(msg)
43
+ if self.revision is not None and not _REVISION.fullmatch(self.revision):
44
+ msg = "corpus snapshot revision must be a full lowercase Git revision"
45
+ raise ValueError(msg)
46
+
47
+ def __repr__(self) -> str:
48
+ if self.private:
49
+ return f"CorpusSnapshot(name={self.name!r}, files={self.files!r}, bytes={self.bytes!r})"
50
+ return (
51
+ f"CorpusSnapshot(name={self.name!r}, digest={self.digest!r}, files={self.files!r}, "
52
+ f"bytes={self.bytes!r}, revision={self.revision!r})"
53
+ )
54
+
55
+
56
+ class VerifiedInventory(NamedTuple):
57
+ snapshot: CorpusSnapshot
58
+ files: tuple[Path, ...]
59
+
60
+
61
+ def snapshot(source: CorpusSource) -> CorpusSnapshot:
62
+ return snapshot_inventory(source, selected_files(source))
63
+
64
+
65
+ def snapshot_inventory(source: CorpusSource, files: tuple[Path, ...]) -> CorpusSnapshot:
66
+ initial_revision = _git_revision(source) if source.kind is CorpusKind.GIT else None
67
+ digest = hashlib.sha256()
68
+ total = 0
69
+ if not files:
70
+ msg = f"corpus {source.report_name} selected no files"
71
+ raise ValueError(msg)
72
+ for path in files:
73
+ relative = path.relative_to(source.root).as_posix().encode()
74
+ before = path.stat()
75
+ size = before.st_size
76
+ digest.update(len(relative).to_bytes(8, "big"))
77
+ digest.update(relative)
78
+ digest.update(size.to_bytes(8, "big"))
79
+ read = 0
80
+ with path.open("rb") as stream:
81
+ while chunk := stream.read(1024 * 1024):
82
+ digest.update(chunk)
83
+ read += len(chunk)
84
+ after = path.stat()
85
+ if read != size or (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) != (
86
+ before.st_dev,
87
+ before.st_ino,
88
+ before.st_size,
89
+ before.st_mtime_ns,
90
+ ):
91
+ relative_name = "<private-file>" if source.visibility.value == "private" else path.relative_to(source.root)
92
+ msg = f"corpus file changed while hashing: {relative_name}"
93
+ raise ValueError(msg)
94
+ total += read
95
+ revision = _git_revision(source) if source.kind is CorpusKind.GIT else None
96
+ if revision != initial_revision:
97
+ msg = f"corpus {source.report_name} revision changed while hashing"
98
+ raise ValueError(msg)
99
+ return CorpusSnapshot(
100
+ source.report_name,
101
+ f"sha256:{digest.hexdigest()}",
102
+ len(files),
103
+ total,
104
+ revision,
105
+ private=source.visibility.value == "private",
106
+ )
107
+
108
+
109
+ def selected_files(source: CorpusSource) -> tuple[Path, ...]:
110
+ if not source.root.is_dir():
111
+ root = "<private-corpus-root>" if source.visibility.value == "private" else source.root
112
+ msg = f"corpus root is not a directory: {root}"
113
+ raise ValueError(msg)
114
+ candidates = _git_tracked_files(source) if source.kind is CorpusKind.GIT else source.root.rglob("*")
115
+ files: list[Path] = []
116
+ for path in candidates:
117
+ if is_link_like(path) or not path.is_file():
118
+ continue
119
+ relative = path.relative_to(source.root).as_posix()
120
+ if _matches(relative, source.include) and not _matches(relative, source.exclude):
121
+ files.append(path)
122
+ return tuple(sorted(files, key=lambda path: path.relative_to(source.root).as_posix()))
123
+
124
+
125
+ def _matches(path: str, patterns: tuple[str, ...]) -> bool:
126
+ return any(
127
+ fnmatch(path, pattern) or (pattern.startswith("**/") and fnmatch(path, pattern[3:])) for pattern in patterns
128
+ )
129
+
130
+
131
+ def verify(source: CorpusSource) -> CorpusSnapshot:
132
+ verified, _files = verify_inventory(source)
133
+ return verified
134
+
135
+
136
+ def verify_inventory(source: CorpusSource) -> VerifiedInventory:
137
+ files = selected_files(source)
138
+ actual = snapshot_inventory(source, files)
139
+ if actual.digest != source.digest:
140
+ if actual.private:
141
+ msg = f"corpus {source.report_name} digest drifted"
142
+ else:
143
+ msg = f"corpus {source.report_name} digest drifted: expected {source.digest}, found {actual.digest}"
144
+ raise ValueError(msg)
145
+ if source.kind is CorpusKind.GIT and actual.revision != source.revision:
146
+ if actual.private:
147
+ msg = f"corpus {source.report_name} revision drifted"
148
+ else:
149
+ msg = f"corpus {source.report_name} revision drifted: expected {source.revision}, found {actual.revision}"
150
+ raise ValueError(msg)
151
+ verified = CorpusSnapshot(
152
+ actual.name,
153
+ actual.digest,
154
+ actual.files,
155
+ actual.bytes,
156
+ actual.revision,
157
+ private=actual.private,
158
+ )
159
+ object.__setattr__(verified, "verified", True) # ruff: ignore[unnecessary-dunder-call] -- frozen evidence token.
160
+ return VerifiedInventory(verified, files)
161
+
162
+
163
+ def _git_revision(source: CorpusSource) -> str:
164
+ executable = shutil.which("git")
165
+ if executable is None:
166
+ msg = "git is required to verify a pinned corpus"
167
+ raise OSError(msg)
168
+ environment = _git_environment()
169
+ completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- fixed local Git query.
170
+ (executable, "rev-parse", "--show-toplevel", "HEAD"),
171
+ cwd=source.root,
172
+ check=False,
173
+ capture_output=True,
174
+ env=environment,
175
+ shell=False,
176
+ text=True,
177
+ )
178
+ lines = completed.stdout.splitlines()
179
+ if completed.returncode or len(lines) != _GIT_REVISION_OUTPUT_LINES:
180
+ msg = f"could not read Git revision for corpus {source.report_name}"
181
+ raise ValueError(msg)
182
+ top_level, revision = lines
183
+ if Path(top_level).resolve() != source.root.resolve():
184
+ msg = f"Git repository root does not match corpus {source.report_name} root"
185
+ raise ValueError(msg)
186
+ return revision
187
+
188
+
189
+ def _git_tracked_files(source: CorpusSource) -> tuple[Path, ...]:
190
+ executable = shutil.which("git")
191
+ if executable is None:
192
+ msg = "git is required to select tracked corpus files"
193
+ raise OSError(msg)
194
+ completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- fixed local Git query.
195
+ (executable, "ls-files", "-z"),
196
+ cwd=source.root,
197
+ check=False,
198
+ capture_output=True,
199
+ env=_git_environment(),
200
+ shell=False,
201
+ text=True,
202
+ )
203
+ if completed.returncode:
204
+ msg = f"could not list tracked files for corpus {source.report_name}"
205
+ raise ValueError(msg)
206
+ return tuple(source.root / relative for relative in completed.stdout.split("\0") if relative)
207
+
208
+
209
+ def _git_environment() -> dict[str, str]:
210
+ environment = {
211
+ name: value
212
+ for name, value in os.environ.items() # ruff: ignore[banned-api] -- discard hook-local repository routing.
213
+ if name in _GIT_SAFE_ENV
214
+ }
215
+ environment.update(
216
+ {
217
+ "GIT_CONFIG_GLOBAL": os.devnull,
218
+ "GIT_CONFIG_NOSYSTEM": "1",
219
+ "GIT_CONFIG_SYSTEM": os.devnull,
220
+ }
221
+ )
222
+ return environment
@@ -0,0 +1,65 @@
1
+ from .models import (
2
+ ANALYSIS_SCHEMA,
3
+ FINGERPRINT_VERSION,
4
+ SCHEMA_URI,
5
+ SCHEMA_VERSION,
6
+ AnalysisReport,
7
+ AnalyzerId,
8
+ CacheStatus,
9
+ Completion,
10
+ Conclusion,
11
+ CoverageDisposition,
12
+ CoverageNotice,
13
+ Diagnostic,
14
+ ExecutionIssue,
15
+ Fix,
16
+ FixSafety,
17
+ InvocationId,
18
+ Location,
19
+ Position,
20
+ Region,
21
+ RelatedLocation,
22
+ RuleDescriptor,
23
+ Severity,
24
+ TextEdit,
25
+ ToolReport,
26
+ TrustMode,
27
+ diagnostic_fingerprint,
28
+ )
29
+ from .serialize import to_github, to_json, to_sarif, to_text
30
+ from .source import SourceDocument
31
+
32
+
33
+ __all__ = [
34
+ "ANALYSIS_SCHEMA",
35
+ "FINGERPRINT_VERSION",
36
+ "SCHEMA_URI",
37
+ "SCHEMA_VERSION",
38
+ "AnalysisReport",
39
+ "AnalyzerId",
40
+ "CacheStatus",
41
+ "Completion",
42
+ "Conclusion",
43
+ "CoverageDisposition",
44
+ "CoverageNotice",
45
+ "Diagnostic",
46
+ "ExecutionIssue",
47
+ "Fix",
48
+ "FixSafety",
49
+ "InvocationId",
50
+ "Location",
51
+ "Position",
52
+ "Region",
53
+ "RelatedLocation",
54
+ "RuleDescriptor",
55
+ "Severity",
56
+ "SourceDocument",
57
+ "TextEdit",
58
+ "ToolReport",
59
+ "TrustMode",
60
+ "diagnostic_fingerprint",
61
+ "to_github",
62
+ "to_json",
63
+ "to_sarif",
64
+ "to_text",
65
+ ]
@@ -0,0 +1,161 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://standards.sarj.ai/schemas/analysis/v1",
4
+ "title": "Sarj Standards analysis report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "schemaVersion", "root", "completion", "conclusion", "exitCode", "rules", "diagnostics", "issues", "tools", "coverage"],
8
+ "properties": {
9
+ "$schema": { "const": "https://standards.sarj.ai/schemas/analysis/v1" },
10
+ "schemaVersion": { "const": 1 },
11
+ "root": { "const": "." },
12
+ "completion": { "enum": ["complete", "partial", "failed"] },
13
+ "conclusion": { "enum": ["passed", "findings", "inconclusive"] },
14
+ "exitCode": { "enum": [0, 1, 2] },
15
+ "rules": { "type": "array", "items": { "$ref": "#/$defs/rule" } },
16
+ "diagnostics": { "type": "array", "items": { "$ref": "#/$defs/diagnostic" } },
17
+ "issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } },
18
+ "tools": { "type": "array", "items": { "$ref": "#/$defs/tool" } },
19
+ "coverage": { "type": "array", "items": { "$ref": "#/$defs/coverage" } }
20
+ },
21
+ "$defs": {
22
+ "position": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["line", "character"],
26
+ "properties": {
27
+ "line": { "type": "integer", "minimum": 0 },
28
+ "character": { "type": "integer", "minimum": 0 }
29
+ }
30
+ },
31
+ "range": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": ["start", "end"],
35
+ "properties": {
36
+ "start": { "$ref": "#/$defs/position" },
37
+ "end": { "$ref": "#/$defs/position" }
38
+ }
39
+ },
40
+ "location": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["path"],
44
+ "properties": {
45
+ "path": { "type": "string", "minLength": 1 },
46
+ "position": { "$ref": "#/$defs/position" },
47
+ "range": { "$ref": "#/$defs/range" }
48
+ },
49
+ "not": { "required": ["position", "range"] }
50
+ },
51
+ "relatedLocation": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "required": ["label", "location"],
55
+ "properties": {
56
+ "label": { "type": "string", "minLength": 1 },
57
+ "location": { "$ref": "#/$defs/location" }
58
+ }
59
+ },
60
+ "edit": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "required": ["location", "replacement"],
64
+ "properties": {
65
+ "location": { "$ref": "#/$defs/location" },
66
+ "replacement": { "type": "string" },
67
+ "expectedTextHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
68
+ }
69
+ },
70
+ "fix": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["title", "safety", "edits"],
74
+ "properties": {
75
+ "title": { "type": "string", "minLength": 1 },
76
+ "safety": { "enum": ["safe", "unsafe"] },
77
+ "edits": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/edit" } }
78
+ }
79
+ },
80
+ "rule": {
81
+ "type": "object",
82
+ "additionalProperties": false,
83
+ "required": ["key", "name", "summary"],
84
+ "properties": {
85
+ "key": { "type": "string", "minLength": 1 },
86
+ "name": { "type": "string", "minLength": 1 },
87
+ "summary": { "type": "string", "minLength": 1 },
88
+ "helpUrl": { "type": "string", "pattern": "^https://" },
89
+ "tags": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
90
+ }
91
+ },
92
+ "fingerprint": {
93
+ "type": "object",
94
+ "additionalProperties": false,
95
+ "required": ["algorithm", "value"],
96
+ "properties": {
97
+ "algorithm": { "const": 1 },
98
+ "value": { "type": "string", "minLength": 1 }
99
+ }
100
+ },
101
+ "diagnostic": {
102
+ "type": "object",
103
+ "additionalProperties": false,
104
+ "required": ["code", "ruleKey", "message", "severity", "source", "location"],
105
+ "properties": {
106
+ "code": { "type": "string", "minLength": 1 },
107
+ "ruleId": { "type": "string", "minLength": 1 },
108
+ "ruleKey": { "type": "string", "minLength": 1 },
109
+ "message": { "type": "string" },
110
+ "severity": { "enum": ["error", "warning", "info"] },
111
+ "source": { "type": "string", "minLength": 1 },
112
+ "location": { "$ref": "#/$defs/location" },
113
+ "help": { "type": "string" },
114
+ "helpUrl": { "type": "string", "pattern": "^https://" },
115
+ "relatedLocations": { "type": "array", "items": { "$ref": "#/$defs/relatedLocation" } },
116
+ "notes": { "type": "array", "items": { "type": "string", "minLength": 1 } },
117
+ "fixes": { "type": "array", "items": { "$ref": "#/$defs/fix" } },
118
+ "tags": { "type": "array", "items": { "type": "string", "minLength": 1 } },
119
+ "fingerprint": { "$ref": "#/$defs/fingerprint" }
120
+ }
121
+ },
122
+ "issue": {
123
+ "type": "object",
124
+ "additionalProperties": false,
125
+ "required": ["source", "kind", "message"],
126
+ "properties": {
127
+ "source": { "type": "string", "minLength": 1 },
128
+ "kind": { "type": "string", "minLength": 1 },
129
+ "message": { "type": "string" },
130
+ "exitCode": { "type": "integer" }
131
+ }
132
+ },
133
+ "tool": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "required": ["name", "completion", "diagnosticCount", "issueCount", "analyzerId", "invocationId", "cache"],
137
+ "properties": {
138
+ "name": { "type": "string", "minLength": 1 },
139
+ "completion": { "enum": ["complete", "partial", "failed"] },
140
+ "diagnosticCount": { "type": "integer", "minimum": 0 },
141
+ "issueCount": { "type": "integer", "minimum": 0 },
142
+ "analyzerId": { "type": "string", "minLength": 1 },
143
+ "invocationId": { "type": "string", "minLength": 1 },
144
+ "version": { "type": "string", "minLength": 1 },
145
+ "fileCount": { "type": "integer", "minimum": 0 },
146
+ "cache": { "enum": ["disabled", "hit", "miss"] }
147
+ }
148
+ },
149
+ "coverage": {
150
+ "type": "object",
151
+ "additionalProperties": false,
152
+ "required": ["source", "reason", "fileCount", "disposition"],
153
+ "properties": {
154
+ "source": { "type": "string", "minLength": 1 },
155
+ "reason": { "type": "string", "minLength": 1 },
156
+ "fileCount": { "type": "integer", "minimum": 1 },
157
+ "disposition": { "enum": ["failed", "unsupported", "excluded", "not-requested"] }
158
+ }
159
+ }
160
+ }
161
+ }