sourcebound 1.2.1__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.
- clean_docs/__init__.py +26 -0
- clean_docs/__main__.py +3 -0
- clean_docs/accessibility.py +182 -0
- clean_docs/adapters/__init__.py +1 -0
- clean_docs/adapters/event_capture.py +56 -0
- clean_docs/adapters/mdx_dependencies.json +714 -0
- clean_docs/adapters/mdx_parser.mjs +29992 -0
- clean_docs/applicability.py +330 -0
- clean_docs/audit.py +1120 -0
- clean_docs/bootstrap.py +507 -0
- clean_docs/capabilities.py +200 -0
- clean_docs/changed.py +452 -0
- clean_docs/claims.py +840 -0
- clean_docs/cli.py +1612 -0
- clean_docs/context.py +307 -0
- clean_docs/corpus.py +377 -0
- clean_docs/demo.py +369 -0
- clean_docs/doctor.py +184 -0
- clean_docs/emit/__init__.py +4 -0
- clean_docs/emit/llms_txt.py +102 -0
- clean_docs/emit/stepwise.py +168 -0
- clean_docs/engine.py +324 -0
- clean_docs/errors.py +20 -0
- clean_docs/evaluation.py +867 -0
- clean_docs/execution.py +138 -0
- clean_docs/explain.py +123 -0
- clean_docs/extractors/__init__.py +19 -0
- clean_docs/extractors/command.py +51 -0
- clean_docs/extractors/inventory.py +176 -0
- clean_docs/extractors/json_pointer.py +84 -0
- clean_docs/extractors/python_literal.py +104 -0
- clean_docs/extractors/static.py +111 -0
- clean_docs/feedback.py +1390 -0
- clean_docs/impact.py +1624 -0
- clean_docs/improvements.py +1178 -0
- clean_docs/inventory.py +474 -0
- clean_docs/isolation.py +157 -0
- clean_docs/manifest.py +898 -0
- clean_docs/mdx.py +272 -0
- clean_docs/migration.py +121 -0
- clean_docs/models.py +194 -0
- clean_docs/outcomes.py +296 -0
- clean_docs/performance.py +123 -0
- clean_docs/phrasing.py +448 -0
- clean_docs/plugins.py +249 -0
- clean_docs/policy.py +536 -0
- clean_docs/projections.py +255 -0
- clean_docs/regions.py +75 -0
- clean_docs/release.py +232 -0
- clean_docs/renderers.py +57 -0
- clean_docs/residue.py +311 -0
- clean_docs/review_contracts.py +862 -0
- clean_docs/review_ledger.py +297 -0
- clean_docs/review_limits.py +9 -0
- clean_docs/sensitivity.py +602 -0
- clean_docs/snapshot.py +212 -0
- clean_docs/standard.py +281 -0
- clean_docs/standards/default.json +309 -0
- clean_docs/standards/exemplars.md +87 -0
- clean_docs/standards/v0-migrations.json +26 -0
- clean_docs/symbols.py +47 -0
- clean_docs/templates.py +96 -0
- clean_docs/verdict.py +1397 -0
- clean_docs/visuals.py +346 -0
- clean_docs/write_gate.py +170 -0
- sourcebound-1.2.1.dist-info/LICENSE +21 -0
- sourcebound-1.2.1.dist-info/METADATA +109 -0
- sourcebound-1.2.1.dist-info/RECORD +71 -0
- sourcebound-1.2.1.dist-info/WHEEL +5 -0
- sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
- sourcebound-1.2.1.dist-info/top_level.txt +1 -0
clean_docs/residue.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import stat
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from clean_docs.errors import ConfigurationError
|
|
16
|
+
from clean_docs.policy import PolicyFinding
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CONFIG_NAME = ".sourcebound-residue.yml"
|
|
20
|
+
LOCAL_CONFIG_NAME = ".sourcebound-residue.local.yml"
|
|
21
|
+
ROOT_KEYS = {"version", "exclude", "rules"}
|
|
22
|
+
RULE_KEYS = {"id", "token_sha256", "include", "reason"}
|
|
23
|
+
LOCAL_RULE_KEYS = {"id", "token", "include"}
|
|
24
|
+
EXCLUDE_KEYS = {"pattern", "reason"}
|
|
25
|
+
TOKEN = re.compile(r"[A-Za-z][A-Za-z0-9_-]*")
|
|
26
|
+
POLICY_METADATA = re.compile(r"^\s*(?:-\s+)?(?:id|pattern|reason):\s*(?P<value>.*)$")
|
|
27
|
+
LOCAL_PATH = re.compile(
|
|
28
|
+
r"(?<![A-Za-z0-9_])/(?:Users|home)/(?P<owner>[^/\s]+)/"
|
|
29
|
+
)
|
|
30
|
+
LOCAL_PATH_PLACEHOLDERS = {
|
|
31
|
+
"example",
|
|
32
|
+
"me",
|
|
33
|
+
"user",
|
|
34
|
+
"username",
|
|
35
|
+
"you",
|
|
36
|
+
"your_username",
|
|
37
|
+
}
|
|
38
|
+
SHA256 = re.compile(r"[0-9a-f]{64}")
|
|
39
|
+
GENERATED_PARTS = {"__pycache__", ".DS_Store"}
|
|
40
|
+
GENERATED_SUFFIXES = {".pyc", ".pyo"}
|
|
41
|
+
MAX_TEXT_BYTES = 1_000_000
|
|
42
|
+
MAX_LOCAL_BYTES = 64 * 1024
|
|
43
|
+
MAX_LOCAL_RULES = 128
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ResidueRule:
|
|
48
|
+
id: str
|
|
49
|
+
token_sha256: str
|
|
50
|
+
include: tuple[str, ...]
|
|
51
|
+
reason: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ResidueExclusion:
|
|
56
|
+
pattern: str
|
|
57
|
+
reason: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ResidueConfig:
|
|
62
|
+
rules: tuple[ResidueRule, ...]
|
|
63
|
+
exclude: tuple[ResidueExclusion, ...]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class LocalResidueRule:
|
|
68
|
+
id: str
|
|
69
|
+
token: str
|
|
70
|
+
include: tuple[str, ...]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _mapping(value: Any, where: str) -> dict[str, Any]:
|
|
74
|
+
if not isinstance(value, dict):
|
|
75
|
+
raise ConfigurationError(f"{where} must be a mapping")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _nonempty(value: Any, where: str) -> str:
|
|
80
|
+
if not isinstance(value, str) or not value.strip():
|
|
81
|
+
raise ConfigurationError(f"{where} must be a non-empty string")
|
|
82
|
+
return value.strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _reject_unknown(value: dict[str, Any], allowed: set[str], where: str) -> None:
|
|
86
|
+
unknown = sorted(set(value) - allowed)
|
|
87
|
+
if unknown:
|
|
88
|
+
raise ConfigurationError(f"{where} has unknown key(s): {', '.join(unknown)}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_residue_config(path: Path) -> ResidueConfig:
|
|
92
|
+
if not path.exists():
|
|
93
|
+
return ResidueConfig((), ())
|
|
94
|
+
try:
|
|
95
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise ConfigurationError(f"cannot read residue config {path}: {exc}") from exc
|
|
98
|
+
except yaml.YAMLError as exc:
|
|
99
|
+
raise ConfigurationError(f"invalid YAML in residue config {path}: {exc}") from exc
|
|
100
|
+
root = _mapping(raw, "residue config")
|
|
101
|
+
_reject_unknown(root, ROOT_KEYS, "residue config")
|
|
102
|
+
version = root.get("version")
|
|
103
|
+
if version not in {1, 2}:
|
|
104
|
+
raise ConfigurationError("residue config version must be 1 or 2")
|
|
105
|
+
if version == 2 and "rules" in root:
|
|
106
|
+
raise ConfigurationError("residue config version 2 permits exclusions only")
|
|
107
|
+
|
|
108
|
+
rules = []
|
|
109
|
+
identifiers: set[str] = set()
|
|
110
|
+
raw_rules = root.get("rules", [])
|
|
111
|
+
if not isinstance(raw_rules, list):
|
|
112
|
+
raise ConfigurationError("residue config rules must be a list")
|
|
113
|
+
for index, item in enumerate(raw_rules):
|
|
114
|
+
where = f"residue config rules[{index}]"
|
|
115
|
+
data = _mapping(item, where)
|
|
116
|
+
_reject_unknown(data, RULE_KEYS, where)
|
|
117
|
+
identifier = _nonempty(data.get("id"), f"{where}.id")
|
|
118
|
+
if identifier in identifiers:
|
|
119
|
+
raise ConfigurationError(f"duplicate residue rule id: {identifier}")
|
|
120
|
+
identifiers.add(identifier)
|
|
121
|
+
digest = _nonempty(data.get("token_sha256"), f"{where}.token_sha256")
|
|
122
|
+
if not SHA256.fullmatch(digest):
|
|
123
|
+
raise ConfigurationError(f"{where}.token_sha256 must be a lowercase SHA-256")
|
|
124
|
+
include = data.get("include")
|
|
125
|
+
if not isinstance(include, list) or not include or not all(
|
|
126
|
+
isinstance(pattern, str) and pattern for pattern in include
|
|
127
|
+
):
|
|
128
|
+
raise ConfigurationError(f"{where}.include must be a non-empty string list")
|
|
129
|
+
reason = _nonempty(data.get("reason"), f"{where}.reason")
|
|
130
|
+
if len(reason) < 12:
|
|
131
|
+
raise ConfigurationError(f"{where}.reason must explain the exclusion")
|
|
132
|
+
rules.append(ResidueRule(identifier, digest, tuple(include), reason))
|
|
133
|
+
|
|
134
|
+
exclusions = []
|
|
135
|
+
raw_exclusions = root.get("exclude", [])
|
|
136
|
+
if not isinstance(raw_exclusions, list):
|
|
137
|
+
raise ConfigurationError("residue config exclude must be a list")
|
|
138
|
+
for index, item in enumerate(raw_exclusions):
|
|
139
|
+
where = f"residue config exclude[{index}]"
|
|
140
|
+
data = _mapping(item, where)
|
|
141
|
+
_reject_unknown(data, EXCLUDE_KEYS, where)
|
|
142
|
+
pattern = _nonempty(data.get("pattern"), f"{where}.pattern")
|
|
143
|
+
reason = _nonempty(data.get("reason"), f"{where}.reason")
|
|
144
|
+
if len(reason) < 12:
|
|
145
|
+
raise ConfigurationError(f"{where}.reason must explain the exclusion")
|
|
146
|
+
exclusions.append(ResidueExclusion(pattern, reason))
|
|
147
|
+
return ResidueConfig(tuple(rules), tuple(exclusions))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_local_residue_rules(path: Path) -> tuple[LocalResidueRule, ...]:
|
|
151
|
+
"""Load untracked plaintext residue rules without exposing their values."""
|
|
152
|
+
if not path.exists():
|
|
153
|
+
return ()
|
|
154
|
+
try:
|
|
155
|
+
metadata = path.lstat()
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
raise ConfigurationError(f"cannot inspect local residue config: {exc}") from exc
|
|
158
|
+
if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
|
|
159
|
+
raise ConfigurationError("local residue config must be a regular file")
|
|
160
|
+
if os.name == "posix" and stat.S_IMODE(metadata.st_mode) != 0o600:
|
|
161
|
+
raise ConfigurationError("local residue config must have mode 0600")
|
|
162
|
+
if metadata.st_size > MAX_LOCAL_BYTES:
|
|
163
|
+
raise ConfigurationError("local residue config exceeds 64 KiB")
|
|
164
|
+
try:
|
|
165
|
+
root = _mapping(yaml.safe_load(path.read_text(encoding="utf-8")), "local residue config")
|
|
166
|
+
except OSError as exc:
|
|
167
|
+
raise ConfigurationError(f"cannot read local residue config: {exc}") from exc
|
|
168
|
+
except yaml.YAMLError as exc:
|
|
169
|
+
raise ConfigurationError(f"invalid YAML in local residue config: {exc}") from exc
|
|
170
|
+
_reject_unknown(root, {"version", "rules"}, "local residue config")
|
|
171
|
+
if root.get("version") != 1:
|
|
172
|
+
raise ConfigurationError("local residue config version must be 1")
|
|
173
|
+
raw_rules = root.get("rules", [])
|
|
174
|
+
if not isinstance(raw_rules, list) or len(raw_rules) > MAX_LOCAL_RULES:
|
|
175
|
+
raise ConfigurationError("local residue config permits at most 128 rules")
|
|
176
|
+
rules = []
|
|
177
|
+
identifiers: set[str] = set()
|
|
178
|
+
for index, item in enumerate(raw_rules):
|
|
179
|
+
where = f"local residue config rules[{index}]"
|
|
180
|
+
data = _mapping(item, where)
|
|
181
|
+
_reject_unknown(data, LOCAL_RULE_KEYS, where)
|
|
182
|
+
identifier = _nonempty(data.get("id"), f"{where}.id")
|
|
183
|
+
if identifier in identifiers:
|
|
184
|
+
raise ConfigurationError(f"duplicate local residue rule id: {identifier}")
|
|
185
|
+
identifiers.add(identifier)
|
|
186
|
+
token = _nonempty(data.get("token"), f"{where}.token")
|
|
187
|
+
include = data.get("include")
|
|
188
|
+
if not isinstance(include, list) or not include or not all(isinstance(value, str) and value for value in include):
|
|
189
|
+
raise ConfigurationError(f"{where}.include must be a non-empty string list")
|
|
190
|
+
rules.append(LocalResidueRule(identifier, token.lower(), tuple(include)))
|
|
191
|
+
return tuple(rules)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _repository_files(root: Path) -> list[Path]:
|
|
195
|
+
proc = subprocess.run(
|
|
196
|
+
["git", "-C", str(root), "ls-files", "-z"],
|
|
197
|
+
capture_output=True,
|
|
198
|
+
timeout=30,
|
|
199
|
+
check=False,
|
|
200
|
+
)
|
|
201
|
+
if proc.returncode == 0:
|
|
202
|
+
return [root / path for path in proc.stdout.decode().split("\0") if path]
|
|
203
|
+
skipped = {".git", ".venv", "node_modules", "build", "dist", "__pycache__"}
|
|
204
|
+
return sorted(
|
|
205
|
+
path for path in root.rglob("*")
|
|
206
|
+
if path.is_file() and not set(path.relative_to(root).parts) & skipped
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _matches(path: str, patterns: tuple[str, ...]) -> bool:
|
|
211
|
+
return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _machine_path_match(line: str) -> bool:
|
|
215
|
+
for match in LOCAL_PATH.finditer(line):
|
|
216
|
+
owner = match.group("owner").strip("<>{}$").lower()
|
|
217
|
+
if owner not in LOCAL_PATH_PLACEHOLDERS:
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _policy_metadata_findings(
|
|
223
|
+
root: Path,
|
|
224
|
+
path: Path,
|
|
225
|
+
config: ResidueConfig,
|
|
226
|
+
local_rules: tuple[LocalResidueRule, ...],
|
|
227
|
+
) -> list[PolicyFinding]:
|
|
228
|
+
"""Check public policy labels without exposing restricted rule material."""
|
|
229
|
+
try:
|
|
230
|
+
relative = path.resolve().relative_to(root).as_posix()
|
|
231
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
232
|
+
except (OSError, ValueError):
|
|
233
|
+
return []
|
|
234
|
+
findings = []
|
|
235
|
+
for line_number, line in enumerate(lines, start=1):
|
|
236
|
+
match = POLICY_METADATA.match(line)
|
|
237
|
+
if match is None:
|
|
238
|
+
continue
|
|
239
|
+
tokens = {token.lower() for token in TOKEN.findall(match.group("value"))}
|
|
240
|
+
digests = {
|
|
241
|
+
hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
242
|
+
for token in tokens
|
|
243
|
+
}
|
|
244
|
+
if any(rule.token_sha256 in digests for rule in config.rules) or any(
|
|
245
|
+
rule.token in tokens for rule in local_rules
|
|
246
|
+
):
|
|
247
|
+
findings.append(PolicyFinding(
|
|
248
|
+
relative,
|
|
249
|
+
line_number,
|
|
250
|
+
"residue-policy-metadata",
|
|
251
|
+
"remove restricted context from residue policy metadata",
|
|
252
|
+
))
|
|
253
|
+
return findings
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def scan_residue(root: Path, config_path: Path | None = None) -> list[PolicyFinding]:
|
|
257
|
+
"""Scan the tracked product surface for configured tokens and machine residue."""
|
|
258
|
+
root = root.resolve()
|
|
259
|
+
policy_path = config_path or root / CONFIG_NAME
|
|
260
|
+
config = load_residue_config(policy_path)
|
|
261
|
+
local_rules = load_local_residue_rules(root / LOCAL_CONFIG_NAME)
|
|
262
|
+
findings = _policy_metadata_findings(root, policy_path, config, local_rules)
|
|
263
|
+
exclusions = tuple(item.pattern for item in config.exclude)
|
|
264
|
+
for path in _repository_files(root):
|
|
265
|
+
relative = path.relative_to(root).as_posix()
|
|
266
|
+
if relative == LOCAL_CONFIG_NAME:
|
|
267
|
+
continue
|
|
268
|
+
if _matches(relative, exclusions):
|
|
269
|
+
continue
|
|
270
|
+
if set(path.parts) & GENERATED_PARTS or path.suffix in GENERATED_SUFFIXES:
|
|
271
|
+
findings.append(PolicyFinding(
|
|
272
|
+
relative, 1, "generated-artifact", "remove generated runtime residue from git"
|
|
273
|
+
))
|
|
274
|
+
continue
|
|
275
|
+
try:
|
|
276
|
+
content = path.read_bytes()
|
|
277
|
+
except OSError:
|
|
278
|
+
continue
|
|
279
|
+
if len(content) > MAX_TEXT_BYTES or b"\0" in content:
|
|
280
|
+
continue
|
|
281
|
+
text = content.decode("utf-8", errors="replace")
|
|
282
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
283
|
+
if _machine_path_match(line):
|
|
284
|
+
findings.append(PolicyFinding(
|
|
285
|
+
relative,
|
|
286
|
+
line_number,
|
|
287
|
+
"local-path-residue",
|
|
288
|
+
"replace the machine-specific home path with a repository-relative path",
|
|
289
|
+
))
|
|
290
|
+
digests = {
|
|
291
|
+
hashlib.sha256(token.lower().encode("utf-8")).hexdigest()
|
|
292
|
+
for token in TOKEN.findall(line)
|
|
293
|
+
}
|
|
294
|
+
for rule in config.rules:
|
|
295
|
+
if rule.token_sha256 in digests and _matches(relative, rule.include):
|
|
296
|
+
findings.append(PolicyFinding(
|
|
297
|
+
relative,
|
|
298
|
+
line_number,
|
|
299
|
+
"cross-project-residue",
|
|
300
|
+
f"remove token matched by repository residue rule {rule.id!r}",
|
|
301
|
+
))
|
|
302
|
+
for local_rule in local_rules:
|
|
303
|
+
if local_rule.token in {token.lower() for token in TOKEN.findall(line)} and _matches(relative, local_rule.include):
|
|
304
|
+
findings.append(PolicyFinding(
|
|
305
|
+
relative,
|
|
306
|
+
line_number,
|
|
307
|
+
"cross-project-residue",
|
|
308
|
+
f"remove token matched by local repository residue rule {local_rule.id!r}",
|
|
309
|
+
))
|
|
310
|
+
findings.sort(key=lambda finding: (finding.doc, finding.line, finding.rule, finding.detail))
|
|
311
|
+
return findings
|