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.
Files changed (71) hide show
  1. clean_docs/__init__.py +26 -0
  2. clean_docs/__main__.py +3 -0
  3. clean_docs/accessibility.py +182 -0
  4. clean_docs/adapters/__init__.py +1 -0
  5. clean_docs/adapters/event_capture.py +56 -0
  6. clean_docs/adapters/mdx_dependencies.json +714 -0
  7. clean_docs/adapters/mdx_parser.mjs +29992 -0
  8. clean_docs/applicability.py +330 -0
  9. clean_docs/audit.py +1120 -0
  10. clean_docs/bootstrap.py +507 -0
  11. clean_docs/capabilities.py +200 -0
  12. clean_docs/changed.py +452 -0
  13. clean_docs/claims.py +840 -0
  14. clean_docs/cli.py +1612 -0
  15. clean_docs/context.py +307 -0
  16. clean_docs/corpus.py +377 -0
  17. clean_docs/demo.py +369 -0
  18. clean_docs/doctor.py +184 -0
  19. clean_docs/emit/__init__.py +4 -0
  20. clean_docs/emit/llms_txt.py +102 -0
  21. clean_docs/emit/stepwise.py +168 -0
  22. clean_docs/engine.py +324 -0
  23. clean_docs/errors.py +20 -0
  24. clean_docs/evaluation.py +867 -0
  25. clean_docs/execution.py +138 -0
  26. clean_docs/explain.py +123 -0
  27. clean_docs/extractors/__init__.py +19 -0
  28. clean_docs/extractors/command.py +51 -0
  29. clean_docs/extractors/inventory.py +176 -0
  30. clean_docs/extractors/json_pointer.py +84 -0
  31. clean_docs/extractors/python_literal.py +104 -0
  32. clean_docs/extractors/static.py +111 -0
  33. clean_docs/feedback.py +1390 -0
  34. clean_docs/impact.py +1624 -0
  35. clean_docs/improvements.py +1178 -0
  36. clean_docs/inventory.py +474 -0
  37. clean_docs/isolation.py +157 -0
  38. clean_docs/manifest.py +898 -0
  39. clean_docs/mdx.py +272 -0
  40. clean_docs/migration.py +121 -0
  41. clean_docs/models.py +194 -0
  42. clean_docs/outcomes.py +296 -0
  43. clean_docs/performance.py +123 -0
  44. clean_docs/phrasing.py +448 -0
  45. clean_docs/plugins.py +249 -0
  46. clean_docs/policy.py +536 -0
  47. clean_docs/projections.py +255 -0
  48. clean_docs/regions.py +75 -0
  49. clean_docs/release.py +232 -0
  50. clean_docs/renderers.py +57 -0
  51. clean_docs/residue.py +311 -0
  52. clean_docs/review_contracts.py +862 -0
  53. clean_docs/review_ledger.py +297 -0
  54. clean_docs/review_limits.py +9 -0
  55. clean_docs/sensitivity.py +602 -0
  56. clean_docs/snapshot.py +212 -0
  57. clean_docs/standard.py +281 -0
  58. clean_docs/standards/default.json +309 -0
  59. clean_docs/standards/exemplars.md +87 -0
  60. clean_docs/standards/v0-migrations.json +26 -0
  61. clean_docs/symbols.py +47 -0
  62. clean_docs/templates.py +96 -0
  63. clean_docs/verdict.py +1397 -0
  64. clean_docs/visuals.py +346 -0
  65. clean_docs/write_gate.py +170 -0
  66. sourcebound-1.2.1.dist-info/LICENSE +21 -0
  67. sourcebound-1.2.1.dist-info/METADATA +109 -0
  68. sourcebound-1.2.1.dist-info/RECORD +71 -0
  69. sourcebound-1.2.1.dist-info/WHEEL +5 -0
  70. sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
  71. 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