sourcebound 1.2.1rc1__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.1rc1.dist-info/LICENSE +21 -0
  67. sourcebound-1.2.1rc1.dist-info/METADATA +109 -0
  68. sourcebound-1.2.1rc1.dist-info/RECORD +71 -0
  69. sourcebound-1.2.1rc1.dist-info/WHEEL +5 -0
  70. sourcebound-1.2.1rc1.dist-info/entry_points.txt +2 -0
  71. sourcebound-1.2.1rc1.dist-info/top_level.txt +1 -0
clean_docs/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """Bind repository documentation to deterministic sources."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ if sys.version_info >= (3, 11):
8
+ import tomllib
9
+ else: # pragma: no cover - Python 3.10
10
+ import tomli as tomllib
11
+
12
+
13
+ def _package_version() -> str:
14
+ project = Path(__file__).resolve().parents[2] / "pyproject.toml"
15
+ if project.is_file():
16
+ try:
17
+ return str(tomllib.loads(project.read_text(encoding="utf-8"))["project"]["version"])
18
+ except (OSError, KeyError, tomllib.TOMLDecodeError):
19
+ pass
20
+ try:
21
+ return version("sourcebound")
22
+ except PackageNotFoundError:
23
+ return "0+unknown"
24
+
25
+
26
+ __version__ = _package_version()
clean_docs/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from clean_docs.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from clean_docs.policy import PolicyFinding
9
+
10
+
11
+ FENCE = re.compile(r"^\s{0,3}(?P<marker>`{3,}|~{3,})(?P<info>.*)$")
12
+ MARKDOWN_IMAGE = re.compile(r"!\[(?P<alt>[^\]]*)\]\([^)]+\)")
13
+ HTML_IMAGE = re.compile(r"<img\b(?P<attributes>[^>]*)>", re.IGNORECASE | re.DOTALL)
14
+ HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
15
+ MDX_COMMENT = re.compile(r"\{/\*.*?\*/\}", re.DOTALL)
16
+ INLINE_CODE = re.compile(r"(?P<fence>`+)[^\n]*?(?P=fence)")
17
+ ALT_ATTRIBUTE = re.compile(
18
+ r"\balt\s*=\s*(?:\"(?P<double>[^\"]*)\"|'(?P<single>[^']*)')",
19
+ re.IGNORECASE,
20
+ )
21
+ PRESENTATION_ROLE = re.compile(
22
+ r"\brole\s*=\s*(?:\"presentation\"|'presentation')",
23
+ re.IGNORECASE,
24
+ )
25
+ DECORATIVE_MARKERS = {
26
+ "<!-- sourcebound:decorative-image -->",
27
+ "{/* sourcebound:decorative-image */}",
28
+ }
29
+ DIAGRAM_PREFIX = re.compile(r"^Diagram(?: description)?:\s+\S", re.IGNORECASE)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class FenceBlock:
34
+ line: int
35
+ end_line: int
36
+ language: str
37
+
38
+
39
+ def _fences(text: str) -> tuple[FenceBlock, ...]:
40
+ lines = text.splitlines()
41
+ blocks: list[FenceBlock] = []
42
+ open_marker: tuple[str, int, int, str] | None = None
43
+ for line_number, line in enumerate(lines, start=1):
44
+ match = FENCE.match(line)
45
+ if match is None:
46
+ continue
47
+ marker = match.group("marker")
48
+ if open_marker is None:
49
+ info = match.group("info").strip()
50
+ language = info.split(maxsplit=1)[0] if info else ""
51
+ open_marker = (marker[0], len(marker), line_number, language)
52
+ continue
53
+ character, width, start_line, language = open_marker
54
+ if (
55
+ marker[0] == character
56
+ and len(marker) >= width
57
+ and not match.group("info").strip()
58
+ ):
59
+ blocks.append(FenceBlock(start_line, line_number, language))
60
+ open_marker = None
61
+ return tuple(blocks)
62
+
63
+
64
+ def _previous_content(lines: list[str], line_number: int) -> str:
65
+ for line in reversed(lines[: line_number - 1]):
66
+ if line.strip():
67
+ return line.strip()
68
+ return ""
69
+
70
+
71
+ def _next_content(lines: list[str], line_number: int) -> tuple[int, str] | None:
72
+ for index, line in enumerate(lines[line_number:], start=line_number + 1):
73
+ stripped = line.strip()
74
+ if stripped:
75
+ return index, stripped
76
+ return None
77
+
78
+
79
+ def _line_number(text: str, offset: int) -> int:
80
+ return text.count("\n", 0, offset) + 1
81
+
82
+
83
+ def _blank(value: str) -> str:
84
+ return "".join("\n" if character == "\n" else " " for character in value)
85
+
86
+
87
+ def _visible_source(text: str, blocks: tuple[FenceBlock, ...]) -> str:
88
+ lines = text.splitlines(keepends=True)
89
+ fenced_lines = {
90
+ line_number
91
+ for block in blocks
92
+ for line_number in range(block.line, block.end_line + 1)
93
+ }
94
+ visible = "".join(
95
+ _blank(line) if line_number in fenced_lines else line
96
+ for line_number, line in enumerate(lines, start=1)
97
+ )
98
+ visible = HTML_COMMENT.sub(lambda match: _blank(match.group(0)), visible)
99
+ visible = MDX_COMMENT.sub(lambda match: _blank(match.group(0)), visible)
100
+ return INLINE_CODE.sub(lambda match: _blank(match.group(0)), visible)
101
+
102
+
103
+ def check_accessibility(
104
+ doc: str,
105
+ text: str,
106
+ pack: dict[str, Any],
107
+ ) -> list[PolicyFinding]:
108
+ from clean_docs.policy import PolicyFinding
109
+
110
+ checks = tuple(str(check) for check in pack["checklist"])
111
+ lines = text.splitlines()
112
+ findings: list[PolicyFinding] = []
113
+ blocks = _fences(text)
114
+ visible_source = _visible_source(text, blocks)
115
+
116
+ if any("Every fenced code block declares its language" in check for check in checks):
117
+ findings.extend(
118
+ PolicyFinding(
119
+ doc,
120
+ block.line,
121
+ "code-block-language",
122
+ "add a language label; use text for literal output or prompts",
123
+ )
124
+ for block in blocks
125
+ if not block.language
126
+ )
127
+
128
+ if any(
129
+ "Every Mermaid diagram has an adjacent text equivalent" in check
130
+ for check in checks
131
+ ):
132
+ for block in blocks:
133
+ if block.language.lower() != "mermaid":
134
+ continue
135
+ following = _next_content(lines, block.end_line)
136
+ if following is None or not DIAGRAM_PREFIX.match(following[1]):
137
+ findings.append(PolicyFinding(
138
+ doc,
139
+ block.line,
140
+ "diagram-text-equivalent",
141
+ "add an adjacent 'Diagram:' description after the Mermaid block",
142
+ ))
143
+
144
+ if any("Every image has useful alternative text" in check for check in checks):
145
+ for line_number, line in enumerate(visible_source.splitlines(), start=1):
146
+ for match in MARKDOWN_IMAGE.finditer(line):
147
+ if match.group("alt").strip():
148
+ continue
149
+ if _previous_content(lines, line_number) in DECORATIVE_MARKERS:
150
+ continue
151
+ findings.append(PolicyFinding(
152
+ doc,
153
+ line_number,
154
+ "image-alternative",
155
+ "write useful alt text or precede a decorative image with "
156
+ "'<!-- sourcebound:decorative-image -->'",
157
+ ))
158
+ for match in HTML_IMAGE.finditer(visible_source):
159
+ line_number = _line_number(visible_source, match.start())
160
+ attributes = match.group("attributes")
161
+ alt = ALT_ATTRIBUTE.search(attributes)
162
+ if alt is None:
163
+ findings.append(PolicyFinding(
164
+ doc,
165
+ line_number,
166
+ "image-alternative",
167
+ "add an alt attribute; use alt=\"\" and role=\"presentation\" "
168
+ "for a decorative image",
169
+ ))
170
+ continue
171
+ value = alt.group("double")
172
+ if value is None:
173
+ value = alt.group("single")
174
+ if not value and PRESENTATION_ROLE.search(attributes) is None:
175
+ findings.append(PolicyFinding(
176
+ doc,
177
+ line_number,
178
+ "image-alternative",
179
+ "pair an empty alt attribute with role=\"presentation\" or "
180
+ "write useful alt text",
181
+ ))
182
+ return findings
@@ -0,0 +1 @@
1
+ """First-party structural document adapters."""
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.request
7
+ import uuid
8
+ from typing import Any, Mapping
9
+
10
+ from clean_docs.errors import ConfigurationError
11
+
12
+ MAX_RESPONSE_BYTES = 4_096
13
+
14
+
15
+ def deliver_event(
16
+ *,
17
+ endpoint: str,
18
+ token_env: str,
19
+ envelope: Mapping[str, Any],
20
+ ) -> None:
21
+ token = os.environ.get(token_env)
22
+ if not token:
23
+ raise ConfigurationError(
24
+ f"connected feedback token environment variable is unset: {token_env}"
25
+ )
26
+ wire_payload = json.dumps(
27
+ {
28
+ "api_key": token,
29
+ "event": "clean_docs_feedback",
30
+ "distinct_id": envelope["installation_id"],
31
+ "uuid": str(uuid.UUID(hex=envelope["event_id"][:32])),
32
+ "properties": {
33
+ **envelope,
34
+ "$process_person_profile": False,
35
+ },
36
+ "timestamp": envelope["occurred_at"],
37
+ },
38
+ ensure_ascii=False,
39
+ separators=(",", ":"),
40
+ sort_keys=True,
41
+ ).encode()
42
+ request = urllib.request.Request(
43
+ endpoint,
44
+ data=wire_payload,
45
+ method="POST",
46
+ headers={"Content-Type": "application/json"},
47
+ )
48
+ try:
49
+ with urllib.request.urlopen(request, timeout=10) as response:
50
+ response.read(MAX_RESPONSE_BYTES)
51
+ if not 200 <= response.status < 300:
52
+ raise ConfigurationError(
53
+ f"connected feedback sink returned HTTP {response.status}"
54
+ )
55
+ except (OSError, urllib.error.URLError) as exc:
56
+ raise ConfigurationError("connected feedback delivery failed") from exc