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/__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,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
|