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/snapshot.py ADDED
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ import ntpath
4
+ import posixpath
5
+ import subprocess
6
+ import tarfile
7
+ import tempfile
8
+ from contextlib import contextmanager
9
+ from dataclasses import dataclass
10
+ from collections.abc import Iterator
11
+ from fnmatch import fnmatch
12
+ from pathlib import Path, PurePosixPath, PureWindowsPath
13
+
14
+ from clean_docs.errors import ExtractionError
15
+
16
+
17
+ def _archive_path_is_safe(value: str) -> bool:
18
+ posix_path = PurePosixPath(value)
19
+ windows_path = PureWindowsPath(value)
20
+ return not (
21
+ posix_path.is_absolute()
22
+ or windows_path.drive
23
+ or windows_path.root
24
+ or ".." in posix_path.parts
25
+ or ".." in windows_path.parts
26
+ )
27
+
28
+
29
+ def _symlink_target_is_safe(member_name: str, link_name: str) -> bool:
30
+ posix_link = PurePosixPath(link_name)
31
+ windows_link = PureWindowsPath(link_name)
32
+ if (
33
+ posix_link.is_absolute()
34
+ or windows_link.drive
35
+ or windows_link.root
36
+ or ("\\" in link_name and ".." in windows_link.parts)
37
+ ):
38
+ return False
39
+
40
+ posix_target = posixpath.normpath(
41
+ (PurePosixPath(member_name).parent / posix_link).as_posix()
42
+ )
43
+ windows_target = ntpath.normpath(
44
+ str(PureWindowsPath(member_name).parent / windows_link)
45
+ )
46
+ return _archive_path_is_safe(posix_target) and _archive_path_is_safe(
47
+ windows_target
48
+ )
49
+
50
+
51
+ def _member_is_safe(member: tarfile.TarInfo) -> bool:
52
+ if not _archive_path_is_safe(member.name) or member.islnk():
53
+ return False
54
+ if not member.issym():
55
+ return True
56
+ return _symlink_target_is_safe(member.name, member.linkname)
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class RepositorySnapshot:
61
+ root: Path
62
+ ref: str | None = None
63
+
64
+ @property
65
+ def label(self) -> str:
66
+ if self.ref is None:
67
+ return "WORKTREE"
68
+ proc = self._git("rev-parse", "--verify", f"{self.ref}^{{commit}}")
69
+ return proc.stdout.strip()
70
+
71
+ def read_text(self, path: Path) -> str:
72
+ if path.is_absolute() or ".." in path.parts:
73
+ raise ExtractionError(f"source path escapes repository: {path}")
74
+ if self.ref is None:
75
+ target = self.root / path
76
+ try:
77
+ return target.read_text(encoding="utf-8")
78
+ except OSError as exc:
79
+ raise ExtractionError(f"cannot read source {path}: {exc}") from exc
80
+ proc = self._git("show", f"{self.ref}:{path.as_posix()}")
81
+ return proc.stdout
82
+
83
+ def matching_files(self, pattern: str) -> list[Path]:
84
+ candidate = Path(pattern)
85
+ if candidate.is_absolute() or ".." in candidate.parts:
86
+ raise ExtractionError(f"source glob escapes repository: {pattern}")
87
+ if self.ref is None:
88
+ return sorted(
89
+ path.relative_to(self.root)
90
+ for path in self.root.glob(pattern)
91
+ if path.is_file() and self.root in path.resolve().parents
92
+ )
93
+ proc = self._git("ls-tree", "-r", "--name-only", self.ref)
94
+ return [Path(path) for path in proc.stdout.splitlines() if fnmatch(path, pattern)]
95
+
96
+ @contextmanager
97
+ def materialized_root(
98
+ self,
99
+ *,
100
+ paths: tuple[Path, ...] = (),
101
+ ) -> Iterator[Path]:
102
+ if self.ref is None:
103
+ yield self.root
104
+ return
105
+ archive_paths: list[str] = []
106
+ for path in paths:
107
+ if path == Path("."):
108
+ continue
109
+ if path.is_absolute() or ".." in path.parts:
110
+ raise ExtractionError(
111
+ f"snapshot path escapes repository: {path}"
112
+ )
113
+ archive_paths.append(path.as_posix().strip("/"))
114
+ archive_paths = self._include_symlink_targets(archive_paths)
115
+ with tempfile.TemporaryDirectory(prefix="sourcebound-snapshot-") as temporary:
116
+ destination = Path(temporary)
117
+ archive = destination / "snapshot.tar"
118
+ arguments = [
119
+ "archive",
120
+ "--format=tar",
121
+ f"--output={archive}",
122
+ self.label,
123
+ ]
124
+ if archive_paths:
125
+ arguments.extend(("--", *archive_paths))
126
+ self._git(*arguments)
127
+ with tarfile.open(archive) as handle:
128
+ for member in handle.getmembers():
129
+ if not _member_is_safe(member):
130
+ raise ExtractionError(f"unsafe path in repository snapshot: {member.name}")
131
+ handle.extractall(destination)
132
+ archive.unlink()
133
+ yield destination
134
+
135
+ def _include_symlink_targets(self, paths: list[str]) -> list[str]:
136
+ if not paths:
137
+ return paths
138
+ selected = set(paths)
139
+ pending = list(paths)
140
+ while pending:
141
+ candidate = pending.pop()
142
+ entries = self._git(
143
+ "ls-tree",
144
+ "-r",
145
+ "-z",
146
+ self.label,
147
+ "--",
148
+ candidate,
149
+ ).stdout
150
+ for entry in entries.split("\0"):
151
+ if not entry:
152
+ continue
153
+ header, separator, name = entry.partition("\t")
154
+ if not separator or not header.startswith("120000 "):
155
+ continue
156
+ link = self._git(
157
+ "show",
158
+ f"{self.label}:{name}",
159
+ ).stdout.rstrip("\n")
160
+ link_path = PurePosixPath(link)
161
+ if link_path.is_absolute():
162
+ raise ExtractionError(
163
+ f"selected snapshot contains an absolute symlink: {name}"
164
+ )
165
+ target = PurePosixPath(
166
+ posixpath.normpath(
167
+ (PurePosixPath(name).parent / link_path).as_posix()
168
+ )
169
+ )
170
+ if target.is_absolute() or ".." in target.parts:
171
+ raise ExtractionError(
172
+ f"selected snapshot symlink escapes repository: {name}"
173
+ )
174
+ target_name = target.as_posix()
175
+ if target_name == ".":
176
+ raise ExtractionError(
177
+ "selected snapshot symlink expands scope to "
178
+ f"repository root: {name}"
179
+ )
180
+ if any(
181
+ target_name == selected_path
182
+ or target_name.startswith(selected_path + "/")
183
+ for selected_path in selected
184
+ ):
185
+ continue
186
+ target_entry = self._git(
187
+ "ls-tree",
188
+ self.label,
189
+ "--",
190
+ target_name,
191
+ ).stdout
192
+ if not target_entry:
193
+ continue
194
+ selected.add(target_name)
195
+ pending.append(target_name)
196
+ return sorted(selected)
197
+
198
+ def _git(self, *args: str) -> subprocess.CompletedProcess[str]:
199
+ try:
200
+ proc = subprocess.run(
201
+ ["git", "-C", str(self.root), *args],
202
+ text=True,
203
+ capture_output=True,
204
+ timeout=30,
205
+ check=False,
206
+ )
207
+ except (OSError, subprocess.SubprocessError) as exc:
208
+ raise ExtractionError(f"git {' '.join(args)} failed: {exc}") from exc
209
+ if proc.returncode != 0:
210
+ message = proc.stderr.strip() or proc.stdout.strip() or "unknown git error"
211
+ raise ExtractionError(f"git {' '.join(args)} failed: {message}")
212
+ return proc
clean_docs/standard.py ADDED
@@ -0,0 +1,281 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from importlib import resources
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from clean_docs.errors import ConfigurationError
11
+
12
+ PACK_VERSION = 1
13
+ DEFAULT_PROFILE = "sourcebound-default"
14
+ HEADING_RE = re.compile(r"^(#{2,4})\s+(.+?)\s*$")
15
+ CHECK_RE = re.compile(r"^- \[ \]\s+(.+)$")
16
+
17
+
18
+ def _digest(text: str) -> str:
19
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
20
+
21
+
22
+ def _pack_digest(pack: dict[str, Any]) -> str:
23
+ content = {key: value for key, value in pack.items() if key != "pack_sha256"}
24
+ canonical = json.dumps(content, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
25
+ return _digest(canonical)
26
+
27
+
28
+ def _exemplar_asset(standard: Path) -> tuple[str, str]:
29
+ local = standard.parent / "src/clean_docs/standards/exemplars.md"
30
+ if local.exists():
31
+ text = local.read_text(encoding="utf-8")
32
+ else:
33
+ resource = resources.files("clean_docs").joinpath("standards/exemplars.md")
34
+ with resources.as_file(resource) as path:
35
+ text = path.read_text(encoding="utf-8")
36
+ return text, _digest(text)
37
+
38
+
39
+ def _headings(lines: list[str]) -> list[dict[str, Any]]:
40
+ result = []
41
+ for line_number, line in enumerate(lines, start=1):
42
+ match = HEADING_RE.match(line)
43
+ if match:
44
+ result.append({
45
+ "level": len(match.group(1)),
46
+ "title": match.group(2),
47
+ "line": line_number,
48
+ })
49
+ return result
50
+
51
+
52
+ def _checklist(lines: list[str]) -> list[str]:
53
+ start = next(
54
+ (index for index, line in enumerate(lines) if line.startswith("## 8. Pre-publish checklist")),
55
+ None,
56
+ )
57
+ if start is None:
58
+ raise ConfigurationError("standard is missing the pre-publish checklist")
59
+ checks: list[str] = []
60
+ current: list[str] = []
61
+ for line in lines[start + 1:]:
62
+ match = CHECK_RE.match(line)
63
+ if match:
64
+ if current:
65
+ checks.append(" ".join(current))
66
+ current = [match.group(1).strip()]
67
+ elif current and line.startswith(" "):
68
+ current.append(line.strip())
69
+ elif current and not line.strip():
70
+ checks.append(" ".join(current))
71
+ current = []
72
+ if current:
73
+ checks.append(" ".join(current))
74
+ if not checks:
75
+ raise ConfigurationError("standard pre-publish checklist is empty")
76
+ return checks
77
+
78
+
79
+ def _mechanical_policy(text: str, checks: list[str]) -> dict[str, Any]:
80
+ length = re.search(
81
+ r"README pages over (?P<readme>\d+) lines and guides over (?P<guide>\d+) lines",
82
+ text,
83
+ )
84
+ section = re.search(r"A section over (?P<section>\d+) lines", text)
85
+ preamble = re.search(r"first (?P<preamble>\d+) lines", text)
86
+ nominalizations = re.search(
87
+ r"sentence with (?P<nominalizations>\w+) or more abstraction-suffix", text
88
+ )
89
+ qualifiers = re.search(r"gets at most (?P<qualifiers>\w+) `may`", text)
90
+ if not all((length, section, preamble, nominalizations, qualifiers)):
91
+ raise ConfigurationError(
92
+ "standard is missing page budgets, preamble window, or register thresholds"
93
+ )
94
+ number_words = {
95
+ "zero": 0,
96
+ "one": 1,
97
+ "two": 2,
98
+ "three": 3,
99
+ "four": 4,
100
+ "five": 5,
101
+ }
102
+ assert length is not None
103
+ assert section is not None
104
+ assert preamble is not None
105
+ assert nominalizations is not None
106
+ assert qualifiers is not None
107
+ booster_check = next((check for check in checks if "booster adjectives" in check), None)
108
+ if booster_check is None:
109
+ raise ConfigurationError("standard is missing the booster-adjective check")
110
+ boosters = re.findall(r"`([^`]+)`", booster_check)
111
+ if len(boosters) < 3:
112
+ raise ConfigurationError("standard booster-adjective check has no usable registry")
113
+ return {
114
+ "readme_max_lines": int(length.group("readme")),
115
+ "guide_max_lines": int(length.group("guide")),
116
+ "section_max_lines": int(section.group("section")),
117
+ "preamble_window_lines": int(preamble.group("preamble")),
118
+ "nominalization_threshold": number_words[nominalizations.group("nominalizations")],
119
+ "nominalization_allowlist": [
120
+ "documentation",
121
+ "application",
122
+ "section",
123
+ "configuration",
124
+ ],
125
+ "sentence_variance_min_words": 15,
126
+ "sentence_variance_max_words": 35,
127
+ "qualifier_threshold": number_words[qualifiers.group("qualifiers")],
128
+ "significance_phrases": [
129
+ "exactly the",
130
+ "the very",
131
+ "this demonstrates",
132
+ "deliberately",
133
+ "is itself",
134
+ "which is precisely",
135
+ ],
136
+ "prohibited_boosters": boosters,
137
+ "require_grounded_facts": any("factual claim" in check for check in checks),
138
+ "require_definition_first": any("first screen defines" in check for check in checks),
139
+ "require_one_job": any("one job" in check for check in checks),
140
+ "require_purpose_contract": any("BLUF purpose contract" in check for check in checks),
141
+ "require_preamble_contract": any(
142
+ "first 15 lines contain the purpose" in check for check in checks
143
+ ),
144
+ "require_readme_routes": any("README routes decisions" in check for check in checks),
145
+ "require_depth_links": any(
146
+ "explanatory section over 80 words" in check for check in checks
147
+ ),
148
+ }
149
+
150
+
151
+ def _style_contract(text: str, checks: list[str]) -> dict[str, Any]:
152
+ normalized = " ".join(text.split())
153
+ required_phrases = {
154
+ "second_person": "Second person + imperative",
155
+ "system_actor": "Name the system as an actor",
156
+ "informative_clauses": "Every clause adds information",
157
+ "concrete_verbs": "Plain, concrete verbs",
158
+ "direct_facts": "State facts without hedging",
159
+ "senior_colleague": "helpful senior colleague",
160
+ "bounded_personality": "Personality has a budget",
161
+ "bluf": "BLUF purpose contract",
162
+ }
163
+ missing = [name for name, phrase in required_phrases.items() if phrase not in normalized]
164
+ if missing:
165
+ raise ConfigurationError(
166
+ "standard is missing required style trait(s): " + ", ".join(missing)
167
+ )
168
+ if not any("BLUF purpose contract" in check for check in checks):
169
+ raise ConfigurationError("standard checklist is missing the BLUF purpose contract")
170
+ if not any("subject-derived memorable element" in check for check in checks):
171
+ raise ConfigurationError("standard checklist is missing bounded personality review")
172
+ return {
173
+ "precedence": [
174
+ "truth and honesty",
175
+ "grounding",
176
+ "reader budget",
177
+ "register",
178
+ "warmth",
179
+ ],
180
+ "voice": {
181
+ "register": "helpful senior colleague",
182
+ "reader_actions": "second person and imperative",
183
+ "system_behavior": "name the system as an actor and state behavior as fact",
184
+ "sentence_shape": "split claims that need separate evidence or differ in scope",
185
+ "verbs": "plain and concrete",
186
+ "certainty": "direct facts; mark genuine uncertainty explicitly",
187
+ "contractions": "allowed",
188
+ },
189
+ "purpose_contract": {
190
+ "begin_marker": "<!-- sourcebound:purpose -->",
191
+ "end_marker": "<!-- sourcebound:end purpose -->",
192
+ "position": "first meaningful block after the H1",
193
+ "mechanical": [
194
+ "exactly one marked purpose block",
195
+ "purpose block precedes body content",
196
+ "purpose prose does not restate the H1",
197
+ ],
198
+ "judgment": [
199
+ "defines the project-specific subject and intended operator",
200
+ "names the consequential failure or decision the page addresses",
201
+ "states the authority boundary and a falsifiable resulting capability",
202
+ "uses authored language grounded in the implementation and cited sources",
203
+ ],
204
+ },
205
+ }
206
+
207
+
208
+ def compile_standard(path: Path) -> dict[str, Any]:
209
+ try:
210
+ text = path.read_text(encoding="utf-8")
211
+ except OSError as exc:
212
+ raise ConfigurationError(f"cannot read standard {path}: {exc}") from exc
213
+ lines = text.splitlines()
214
+ headings = _headings(lines)
215
+ titles = [heading["title"] for heading in headings]
216
+ required = {
217
+ "voice": "2. Voice at the sentence level",
218
+ "document": "3. How to explain something technical simply (the actual techniques)",
219
+ "corpus": "6. Beyond the single doc: does it earn its existence?",
220
+ "grounding": "7. Grounding: the doc must be true to the code (and stay true)",
221
+ }
222
+ missing = [title for title in required.values() if title not in titles]
223
+ if missing:
224
+ raise ConfigurationError(f"standard is missing required tier heading(s): {', '.join(missing)}")
225
+ checks = _checklist(lines)
226
+ style = _style_contract(text, checks)
227
+ exemplars, exemplar_sha256 = _exemplar_asset(path)
228
+ pack: dict[str, Any] = {
229
+ "pack_version": PACK_VERSION,
230
+ "profile": DEFAULT_PROFILE,
231
+ "source": {
232
+ "name": path.name,
233
+ "sha256": _digest(text),
234
+ },
235
+ "tiers": required,
236
+ "headings": headings,
237
+ "checklist": checks,
238
+ "policy": _mechanical_policy(text, checks),
239
+ "style": style,
240
+ "generation": {
241
+ "instructions": text,
242
+ "constraint": "Phrase only the supplied evidence and preserve its scope.",
243
+ "voice": style["voice"],
244
+ "purpose_contract": style["purpose_contract"],
245
+ "precedence": style["precedence"],
246
+ "exemplars": exemplars,
247
+ "exemplars_sha256": exemplar_sha256,
248
+ },
249
+ }
250
+ pack["pack_sha256"] = _pack_digest(pack)
251
+ return pack
252
+
253
+
254
+ def write_pack(pack: dict[str, Any], path: Path) -> None:
255
+ from clean_docs.regions import atomic_write
256
+
257
+ atomic_write(path, json.dumps(pack, indent=2, ensure_ascii=False) + "\n")
258
+
259
+
260
+ def load_pack(path: Path) -> dict[str, Any]:
261
+ try:
262
+ raw = json.loads(path.read_text(encoding="utf-8"))
263
+ except OSError as exc:
264
+ raise ConfigurationError(f"cannot read policy pack {path}: {exc}") from exc
265
+ except json.JSONDecodeError as exc:
266
+ raise ConfigurationError(f"invalid policy pack {path}: {exc}") from exc
267
+ if not isinstance(raw, dict) or raw.get("pack_version") != PACK_VERSION:
268
+ raise ConfigurationError(f"unsupported policy pack: {path}")
269
+ if raw.get("pack_sha256") != _pack_digest(raw):
270
+ raise ConfigurationError(f"policy pack integrity check failed: {path}")
271
+ return raw
272
+
273
+
274
+ def load_default_pack() -> dict[str, Any]:
275
+ resource = resources.files("clean_docs").joinpath("standards/default.json")
276
+ with resources.as_file(resource) as path:
277
+ return load_pack(path)
278
+
279
+
280
+ def pack_matches_standard(standard: Path, pack_path: Path) -> bool:
281
+ return compile_standard(standard) == load_pack(pack_path)