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/policy.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class PolicyFinding:
|
|
10
|
+
doc: str
|
|
11
|
+
line: int
|
|
12
|
+
rule: str
|
|
13
|
+
detail: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
H1_RE = re.compile(r"^#\s+(.+?)\s*$")
|
|
17
|
+
WORD_RE = re.compile(r"[a-z0-9]+")
|
|
18
|
+
TITLE_FILLER = {
|
|
19
|
+
"a",
|
|
20
|
+
"an",
|
|
21
|
+
"the",
|
|
22
|
+
"this",
|
|
23
|
+
"page",
|
|
24
|
+
"document",
|
|
25
|
+
"guide",
|
|
26
|
+
"describes",
|
|
27
|
+
"explains",
|
|
28
|
+
"defines",
|
|
29
|
+
"covers",
|
|
30
|
+
"is",
|
|
31
|
+
}
|
|
32
|
+
PURPOSE_BEGIN = "<!-- sourcebound:purpose -->"
|
|
33
|
+
PURPOSE_END = "<!-- sourcebound:end purpose -->"
|
|
34
|
+
REGISTER_PROFILE = "<!-- sourcebound:policy register-v2 -->"
|
|
35
|
+
CANNED_PURPOSE_STEMS = (
|
|
36
|
+
"read this page before changing or relying on",
|
|
37
|
+
"defines the repository's current contract for this surface",
|
|
38
|
+
"it gathers the relevant scope and constraints in one place",
|
|
39
|
+
)
|
|
40
|
+
NON_PROSE_STARTS = ("#", "- ", "* ", ">", "|", "```", "![", "[![", "<img", "<picture")
|
|
41
|
+
POLICY_ALLOW = re.compile(
|
|
42
|
+
r'<!--\s*sourcebound:allow\s+([a-z][a-z-]+)\s+reason="([^"]+)"\s*-->'
|
|
43
|
+
)
|
|
44
|
+
POLICY_YIELD = re.compile(
|
|
45
|
+
r'<!--\s*sourcebound:yield\s+rule="([a-z][a-z-]+)"\s+'
|
|
46
|
+
r'to="([a-z][a-z-]+)"\s+reason="([^"]+)"\s*-->'
|
|
47
|
+
)
|
|
48
|
+
ABSTRACTION_SUFFIX = re.compile(r"[a-z]+(?:tion|sion|ment|ance|ence|ivity)\b", re.I)
|
|
49
|
+
QUALIFIER = re.compile(r"\b(?:may|only|unless|except)\b", re.I)
|
|
50
|
+
MARKDOWN_LINK = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
|
51
|
+
INLINE_CODE = re.compile(r"`[^`]*`")
|
|
52
|
+
HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _prose_lines(text: str) -> list[tuple[int, str]]:
|
|
56
|
+
result = []
|
|
57
|
+
in_fence = False
|
|
58
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
59
|
+
if line.startswith("```"):
|
|
60
|
+
in_fence = not in_fence
|
|
61
|
+
continue
|
|
62
|
+
if not in_fence and "slop-ok:" not in line and not line.startswith("#"):
|
|
63
|
+
result.append((line_number, line))
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _title_tokens(text: str) -> set[str]:
|
|
68
|
+
return {
|
|
69
|
+
token
|
|
70
|
+
for token in WORD_RE.findall(text.lower())
|
|
71
|
+
if token not in TITLE_FILLER
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _outside_fences(lines: list[str]) -> list[tuple[int, str]]:
|
|
76
|
+
result: list[tuple[int, str]] = []
|
|
77
|
+
in_fence = False
|
|
78
|
+
for index, line in enumerate(lines):
|
|
79
|
+
if line.startswith("```"):
|
|
80
|
+
in_fence = not in_fence
|
|
81
|
+
continue
|
|
82
|
+
if not in_fence:
|
|
83
|
+
result.append((index, line))
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _rule_allowed(text: str, rule: str) -> bool:
|
|
88
|
+
return any(
|
|
89
|
+
match.group(1) == rule and len(match.group(2).strip()) >= 12
|
|
90
|
+
for match in POLICY_ALLOW.finditer(text)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _yielded_rules(text: str) -> dict[int, set[str]]:
|
|
95
|
+
yielded: dict[int, set[str]] = {}
|
|
96
|
+
lines = text.splitlines()
|
|
97
|
+
valid_winners = {
|
|
98
|
+
"truth-honesty",
|
|
99
|
+
"grounding",
|
|
100
|
+
"reader-budget",
|
|
101
|
+
"register",
|
|
102
|
+
"warmth",
|
|
103
|
+
}
|
|
104
|
+
for match in POLICY_YIELD.finditer(text):
|
|
105
|
+
if match.group(2) not in valid_winners or len(match.group(3).strip()) < 12:
|
|
106
|
+
continue
|
|
107
|
+
end_line = text.count("\n", 0, match.end()) + 1
|
|
108
|
+
for target_line in range(end_line + 1, len(lines) + 1):
|
|
109
|
+
stripped = lines[target_line - 1].strip()
|
|
110
|
+
if not stripped or stripped.startswith("<!--"):
|
|
111
|
+
continue
|
|
112
|
+
yielded.setdefault(target_line, set()).add(match.group(1))
|
|
113
|
+
break
|
|
114
|
+
return yielded
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _preamble_contract(doc: str, text: str, pack: dict[str, Any]) -> list[PolicyFinding]:
|
|
118
|
+
policy = pack["policy"]
|
|
119
|
+
if (
|
|
120
|
+
doc == "<fragment>"
|
|
121
|
+
or REGISTER_PROFILE not in text
|
|
122
|
+
or not policy.get("require_preamble_contract", False)
|
|
123
|
+
or _rule_allowed(text, "preamble-contract")
|
|
124
|
+
):
|
|
125
|
+
return []
|
|
126
|
+
window_size = int(policy["preamble_window_lines"])
|
|
127
|
+
lines = text.splitlines()
|
|
128
|
+
window = lines[:window_size]
|
|
129
|
+
joined = "\n".join(window)
|
|
130
|
+
has_purpose = PURPOSE_BEGIN in window and PURPOSE_END in window
|
|
131
|
+
has_fence = any(
|
|
132
|
+
re.match(r"```(?:bash|sh|shell|console)\b", line, re.I)
|
|
133
|
+
for line in window
|
|
134
|
+
)
|
|
135
|
+
has_bold_route = bool(re.search(r"\*\*\[[^\]]+\]\([^)]+\)\*\*", joined))
|
|
136
|
+
has_proof = (
|
|
137
|
+
any(line.startswith("[![") for line in window)
|
|
138
|
+
or bool(re.search(r"\b(?:sourcebound\s+)?verify\b", joined, re.I))
|
|
139
|
+
or bool(
|
|
140
|
+
re.search(
|
|
141
|
+
r"\b(?:proof|proves|receipt|outcome|verification)\b",
|
|
142
|
+
joined,
|
|
143
|
+
re.I,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
or any(
|
|
147
|
+
re.search(r"\b(?:proof|receipt|result|outcome|verification)\b", label, re.I)
|
|
148
|
+
for label, _target in MARKDOWN_LINK.findall(joined)
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
missing = [
|
|
152
|
+
label
|
|
153
|
+
for label, present in (
|
|
154
|
+
("marked purpose", has_purpose),
|
|
155
|
+
("primary action", has_fence or has_bold_route),
|
|
156
|
+
("proof", has_proof),
|
|
157
|
+
)
|
|
158
|
+
if not present
|
|
159
|
+
]
|
|
160
|
+
if not missing:
|
|
161
|
+
return []
|
|
162
|
+
return [PolicyFinding(
|
|
163
|
+
doc,
|
|
164
|
+
1,
|
|
165
|
+
"preamble-contract",
|
|
166
|
+
f"put {', '.join(missing)} inside the first {window_size} lines",
|
|
167
|
+
)]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _paragraphs(text: str) -> list[tuple[int, str]]:
|
|
171
|
+
paragraphs: list[tuple[int, str]] = []
|
|
172
|
+
current: list[str] = []
|
|
173
|
+
start = 0
|
|
174
|
+
in_fence = False
|
|
175
|
+
visible_text = HTML_COMMENT.sub(
|
|
176
|
+
lambda match: "\n" * match.group(0).count("\n"),
|
|
177
|
+
text,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def flush() -> None:
|
|
181
|
+
nonlocal current, start
|
|
182
|
+
if current:
|
|
183
|
+
paragraphs.append((start, "\n".join(part.strip() for part in current)))
|
|
184
|
+
current = []
|
|
185
|
+
start = 0
|
|
186
|
+
|
|
187
|
+
for line_number, line in enumerate(visible_text.splitlines(), start=1):
|
|
188
|
+
stripped = line.strip()
|
|
189
|
+
if stripped.startswith("```"):
|
|
190
|
+
flush()
|
|
191
|
+
in_fence = not in_fence
|
|
192
|
+
continue
|
|
193
|
+
if (
|
|
194
|
+
in_fence
|
|
195
|
+
or not stripped
|
|
196
|
+
or stripped.startswith(("#", "<!--", "|", "- ", "* ", ">", "![", "[!["))
|
|
197
|
+
):
|
|
198
|
+
flush()
|
|
199
|
+
continue
|
|
200
|
+
if not current:
|
|
201
|
+
start = line_number
|
|
202
|
+
current.append(stripped)
|
|
203
|
+
flush()
|
|
204
|
+
return paragraphs
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _sentences(paragraph: str) -> list[str]:
|
|
208
|
+
return [
|
|
209
|
+
sentence.strip()
|
|
210
|
+
for sentence in re.split(r"(?<=[.!?])\s+", paragraph)
|
|
211
|
+
if sentence.strip()
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _visible_sentence(sentence: str) -> str:
|
|
216
|
+
without_targets = MARKDOWN_LINK.sub(lambda match: match.group(1), sentence)
|
|
217
|
+
return INLINE_CODE.sub("", without_targets)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _section_titles(text: str) -> list[tuple[int, str]]:
|
|
221
|
+
return [
|
|
222
|
+
(line_number, line.lstrip("#").strip().lower())
|
|
223
|
+
for line_number, line in enumerate(text.splitlines(), start=1)
|
|
224
|
+
if line.startswith("##")
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _literal_section(line_number: int, sections: list[tuple[int, str]]) -> bool:
|
|
229
|
+
title = ""
|
|
230
|
+
for section_line, candidate in sections:
|
|
231
|
+
if section_line > line_number:
|
|
232
|
+
break
|
|
233
|
+
title = candidate
|
|
234
|
+
return "limit" in title or "security" in title or "privacy" in title
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _register_findings(doc: str, text: str, pack: dict[str, Any]) -> list[PolicyFinding]:
|
|
238
|
+
if REGISTER_PROFILE not in text:
|
|
239
|
+
return []
|
|
240
|
+
policy = pack["policy"]
|
|
241
|
+
findings: list[PolicyFinding] = []
|
|
242
|
+
allowlist = {
|
|
243
|
+
str(token).lower() for token in policy["nominalization_allowlist"]
|
|
244
|
+
}
|
|
245
|
+
significance = tuple(
|
|
246
|
+
str(phrase).lower() for phrase in policy["significance_phrases"]
|
|
247
|
+
)
|
|
248
|
+
yielded = _yielded_rules(text)
|
|
249
|
+
sections = _section_titles(text)
|
|
250
|
+
qualifier_scope = doc == "README.md" or doc.startswith("docs/learn/")
|
|
251
|
+
for line_number, paragraph in _paragraphs(text):
|
|
252
|
+
sentences = _sentences(paragraph)
|
|
253
|
+
paragraph_yields = yielded.get(line_number, set())
|
|
254
|
+
if (
|
|
255
|
+
len(sentences) >= 3
|
|
256
|
+
and not _rule_allowed(text, "sentence-variance")
|
|
257
|
+
and "sentence-variance" not in paragraph_yields
|
|
258
|
+
):
|
|
259
|
+
counts = [len(WORD_RE.findall(sentence)) for sentence in sentences]
|
|
260
|
+
if all(
|
|
261
|
+
int(policy["sentence_variance_min_words"])
|
|
262
|
+
<= count
|
|
263
|
+
<= int(policy["sentence_variance_max_words"])
|
|
264
|
+
for count in counts
|
|
265
|
+
):
|
|
266
|
+
findings.append(PolicyFinding(
|
|
267
|
+
doc,
|
|
268
|
+
line_number,
|
|
269
|
+
"sentence-variance",
|
|
270
|
+
"add one short sentence beat or combine claims that share evidence",
|
|
271
|
+
))
|
|
272
|
+
offset = 0
|
|
273
|
+
for sentence in sentences:
|
|
274
|
+
sentence_line = line_number + paragraph[:offset].count("\n")
|
|
275
|
+
offset += len(sentence) + 1
|
|
276
|
+
lowered = _visible_sentence(sentence).lower()
|
|
277
|
+
if (
|
|
278
|
+
not _rule_allowed(text, "nominalization-density")
|
|
279
|
+
and "nominalization-density" not in paragraph_yields
|
|
280
|
+
):
|
|
281
|
+
abstractions = [
|
|
282
|
+
token
|
|
283
|
+
for token in ABSTRACTION_SUFFIX.findall(lowered)
|
|
284
|
+
if token not in allowlist
|
|
285
|
+
]
|
|
286
|
+
if len(abstractions) >= int(policy["nominalization_threshold"]):
|
|
287
|
+
findings.append(PolicyFinding(
|
|
288
|
+
doc,
|
|
289
|
+
sentence_line,
|
|
290
|
+
"nominalization-density",
|
|
291
|
+
"replace clustered abstractions with actors and concrete verbs: "
|
|
292
|
+
+ ", ".join(abstractions),
|
|
293
|
+
))
|
|
294
|
+
if (
|
|
295
|
+
not _rule_allowed(text, "significance-narration")
|
|
296
|
+
and "significance-narration" not in paragraph_yields
|
|
297
|
+
and (match := next((phrase for phrase in significance if phrase in lowered), None))
|
|
298
|
+
):
|
|
299
|
+
findings.append(PolicyFinding(
|
|
300
|
+
doc,
|
|
301
|
+
sentence_line,
|
|
302
|
+
"significance-narration",
|
|
303
|
+
f"replace {match!r} with the consequence for the reader",
|
|
304
|
+
))
|
|
305
|
+
if (
|
|
306
|
+
qualifier_scope
|
|
307
|
+
and not _literal_section(sentence_line, sections)
|
|
308
|
+
and not _rule_allowed(text, "qualifier-density")
|
|
309
|
+
and "qualifier-density" not in paragraph_yields
|
|
310
|
+
):
|
|
311
|
+
guards = QUALIFIER.findall(sentence)
|
|
312
|
+
if len(guards) > int(policy["qualifier_threshold"]):
|
|
313
|
+
findings.append(PolicyFinding(
|
|
314
|
+
doc,
|
|
315
|
+
sentence_line,
|
|
316
|
+
"qualifier-density",
|
|
317
|
+
"move a guard to the canonical limits page or split the authority boundary",
|
|
318
|
+
))
|
|
319
|
+
return findings
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _purpose_contract(doc: str, text: str, pack: dict[str, Any]) -> list[PolicyFinding]:
|
|
323
|
+
if (
|
|
324
|
+
REGISTER_PROFILE not in text
|
|
325
|
+
or not pack["policy"].get("require_purpose_contract", False)
|
|
326
|
+
):
|
|
327
|
+
return []
|
|
328
|
+
contract = pack["style"]["purpose_contract"]
|
|
329
|
+
begin = str(contract["begin_marker"])
|
|
330
|
+
end = str(contract["end_marker"])
|
|
331
|
+
lines = text.splitlines()
|
|
332
|
+
structural_lines = _outside_fences(lines)
|
|
333
|
+
headings = [
|
|
334
|
+
(index, match.group(1))
|
|
335
|
+
for index, line in structural_lines
|
|
336
|
+
if (match := H1_RE.match(line))
|
|
337
|
+
]
|
|
338
|
+
if len(headings) != 1:
|
|
339
|
+
return [PolicyFinding(
|
|
340
|
+
doc,
|
|
341
|
+
1,
|
|
342
|
+
"purpose-contract",
|
|
343
|
+
"add one H1 followed immediately by one marked BLUF purpose contract",
|
|
344
|
+
)]
|
|
345
|
+
begin_lines = [index for index, line in structural_lines if line.strip() == begin]
|
|
346
|
+
end_lines = [index for index, line in structural_lines if line.strip() == end]
|
|
347
|
+
if len(begin_lines) != 1 or len(end_lines) != 1 or begin_lines[0] >= end_lines[0]:
|
|
348
|
+
return [PolicyFinding(
|
|
349
|
+
doc,
|
|
350
|
+
headings[0][0] + 1,
|
|
351
|
+
"purpose-contract",
|
|
352
|
+
"add exactly one complete marked BLUF purpose contract after the H1",
|
|
353
|
+
)]
|
|
354
|
+
h1_index, title = headings[0]
|
|
355
|
+
first_body = h1_index + 1
|
|
356
|
+
while first_body < len(lines):
|
|
357
|
+
stripped = lines[first_body].strip()
|
|
358
|
+
if (
|
|
359
|
+
not stripped
|
|
360
|
+
or stripped.startswith("<!-- sourcebound:allow ")
|
|
361
|
+
or stripped == REGISTER_PROFILE
|
|
362
|
+
):
|
|
363
|
+
first_body += 1
|
|
364
|
+
continue
|
|
365
|
+
break
|
|
366
|
+
if first_body != begin_lines[0]:
|
|
367
|
+
return [PolicyFinding(
|
|
368
|
+
doc,
|
|
369
|
+
begin_lines[0] + 1,
|
|
370
|
+
"purpose-contract",
|
|
371
|
+
"move the purpose contract before all body content",
|
|
372
|
+
)]
|
|
373
|
+
body = lines[begin_lines[0] + 1:end_lines[0]]
|
|
374
|
+
prose = " ".join(line.strip() for line in body if line.strip())
|
|
375
|
+
if not prose or any(
|
|
376
|
+
line.lstrip().lower().startswith(NON_PROSE_STARTS)
|
|
377
|
+
for line in body
|
|
378
|
+
if line.strip()
|
|
379
|
+
):
|
|
380
|
+
return [PolicyFinding(
|
|
381
|
+
doc,
|
|
382
|
+
begin_lines[0] + 1,
|
|
383
|
+
"purpose-contract",
|
|
384
|
+
"write the purpose contract as one plain prose block",
|
|
385
|
+
)]
|
|
386
|
+
lowered_prose = prose.lower()
|
|
387
|
+
if any(stem in lowered_prose for stem in CANNED_PURPOSE_STEMS):
|
|
388
|
+
return [PolicyFinding(
|
|
389
|
+
doc,
|
|
390
|
+
begin_lines[0] + 2,
|
|
391
|
+
"purpose-contract",
|
|
392
|
+
"replace stock purpose language with the subject, operator, consequential failure, and authority boundary",
|
|
393
|
+
)]
|
|
394
|
+
first_sentence = re.split(r"(?<=[.!?])\s+", prose, maxsplit=1)[0]
|
|
395
|
+
title_tokens = _title_tokens(title)
|
|
396
|
+
sentence_tokens = _title_tokens(first_sentence)
|
|
397
|
+
if title_tokens and title_tokens <= sentence_tokens and len(sentence_tokens - title_tokens) < 3:
|
|
398
|
+
return [PolicyFinding(
|
|
399
|
+
doc,
|
|
400
|
+
begin_lines[0] + 2,
|
|
401
|
+
"purpose-contract",
|
|
402
|
+
"replace the title restatement with applicability, problem, and outcome",
|
|
403
|
+
)]
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _substantive_purpose(lines: list[str]) -> bool:
|
|
408
|
+
prose = " ".join(line.strip() for line in lines).strip()
|
|
409
|
+
words = WORD_RE.findall(prose.lower())
|
|
410
|
+
if len(words) < 8:
|
|
411
|
+
return False
|
|
412
|
+
if all(re.match(r"^(?:\*\*)?[^:]{1,32}:(?:\*\*)?\s*", line.strip()) for line in lines):
|
|
413
|
+
return False
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def ensure_purpose_contract(text: str, *, fallback: bool = False) -> str:
|
|
418
|
+
"""Move substantive authored prose to the opening contract or add an optional fallback."""
|
|
419
|
+
if PURPOSE_BEGIN in text or PURPOSE_END in text:
|
|
420
|
+
return text
|
|
421
|
+
lines = text.splitlines()
|
|
422
|
+
heading = next(
|
|
423
|
+
((index, match.group(1)) for index, line in _outside_fences(lines) if (match := H1_RE.match(line))),
|
|
424
|
+
None,
|
|
425
|
+
)
|
|
426
|
+
if heading is None:
|
|
427
|
+
return text
|
|
428
|
+
h1_index, title = heading
|
|
429
|
+
title_tokens = _title_tokens(title)
|
|
430
|
+
selected: tuple[int, int, list[str]] | None = None
|
|
431
|
+
rejected_restatements: list[tuple[int, int]] = []
|
|
432
|
+
index = h1_index + 1
|
|
433
|
+
in_fence = False
|
|
434
|
+
while index < len(lines):
|
|
435
|
+
stripped = lines[index].strip()
|
|
436
|
+
if stripped.startswith("## "):
|
|
437
|
+
break
|
|
438
|
+
if stripped.startswith("```"):
|
|
439
|
+
in_fence = not in_fence
|
|
440
|
+
index += 1
|
|
441
|
+
continue
|
|
442
|
+
if (
|
|
443
|
+
in_fence
|
|
444
|
+
or not stripped
|
|
445
|
+
or stripped.startswith("<!--")
|
|
446
|
+
or stripped.lower().startswith(NON_PROSE_STARTS)
|
|
447
|
+
):
|
|
448
|
+
index += 1
|
|
449
|
+
continue
|
|
450
|
+
end = index
|
|
451
|
+
while end < len(lines) and lines[end].strip():
|
|
452
|
+
candidate = lines[end].lstrip()
|
|
453
|
+
if candidate.lower().startswith(NON_PROSE_STARTS) or candidate.startswith("<!--"):
|
|
454
|
+
break
|
|
455
|
+
end += 1
|
|
456
|
+
paragraph = lines[index:end]
|
|
457
|
+
opening = " ".join(line.strip() for line in paragraph)
|
|
458
|
+
opening_tokens = _title_tokens(
|
|
459
|
+
re.split(r"(?<=[.!?])\s+", opening, maxsplit=1)[0]
|
|
460
|
+
)
|
|
461
|
+
restates_title = (
|
|
462
|
+
bool(title_tokens)
|
|
463
|
+
and title_tokens <= opening_tokens
|
|
464
|
+
and len(opening_tokens - title_tokens) < 3
|
|
465
|
+
)
|
|
466
|
+
if not restates_title:
|
|
467
|
+
if fallback or _substantive_purpose(paragraph):
|
|
468
|
+
selected = (index, end, paragraph)
|
|
469
|
+
break
|
|
470
|
+
else:
|
|
471
|
+
rejected_restatements.append((index, end))
|
|
472
|
+
index = max(end, index + 1)
|
|
473
|
+
|
|
474
|
+
insertion = h1_index + 1
|
|
475
|
+
while insertion < len(lines) and (
|
|
476
|
+
not lines[insertion].strip()
|
|
477
|
+
or lines[insertion].strip().startswith("<!-- sourcebound:allow ")
|
|
478
|
+
):
|
|
479
|
+
insertion += 1
|
|
480
|
+
if selected is None:
|
|
481
|
+
if not fallback:
|
|
482
|
+
return text
|
|
483
|
+
return text
|
|
484
|
+
else:
|
|
485
|
+
start, end, purpose_lines = selected
|
|
486
|
+
remaining = lines[:start] + lines[end:]
|
|
487
|
+
if start < insertion:
|
|
488
|
+
insertion -= end - start
|
|
489
|
+
replacement = [PURPOSE_BEGIN, *purpose_lines, PURPOSE_END]
|
|
490
|
+
updated = remaining[:insertion] + replacement + remaining[insertion:]
|
|
491
|
+
return "\n".join(updated).rstrip() + "\n"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def check_prose(doc: str, text: str, pack: dict[str, Any]) -> list[PolicyFinding]:
|
|
495
|
+
if REGISTER_PROFILE not in text:
|
|
496
|
+
return []
|
|
497
|
+
policy = pack["policy"]
|
|
498
|
+
boosters = tuple(str(word) for word in policy["prohibited_boosters"])
|
|
499
|
+
pattern = re.compile(r"\b(?:" + "|".join(re.escape(word) for word in boosters) + r")\b", re.I)
|
|
500
|
+
findings: list[PolicyFinding] = []
|
|
501
|
+
for line_number, line in _prose_lines(text):
|
|
502
|
+
match = pattern.search(line)
|
|
503
|
+
if match:
|
|
504
|
+
findings.append(PolicyFinding(
|
|
505
|
+
doc=doc,
|
|
506
|
+
line=line_number,
|
|
507
|
+
rule="prohibited-booster",
|
|
508
|
+
detail=f"remove {match.group(0)!r} or state the claim directly",
|
|
509
|
+
))
|
|
510
|
+
return findings
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def check_document(
|
|
514
|
+
doc: str,
|
|
515
|
+
text: str,
|
|
516
|
+
pack: dict[str, Any],
|
|
517
|
+
*,
|
|
518
|
+
semantic_text: str | None = None,
|
|
519
|
+
) -> list[PolicyFinding]:
|
|
520
|
+
from clean_docs.accessibility import check_accessibility
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
_purpose_contract(doc, text, pack)
|
|
524
|
+
+ _preamble_contract(doc, text, pack)
|
|
525
|
+
+ check_prose(doc, text, pack)
|
|
526
|
+
+ _register_findings(doc, text, pack)
|
|
527
|
+
+ check_accessibility(doc, semantic_text if semantic_text is not None else text, pack)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def check_documents(documents: dict[str, str], pack: dict[str, Any]) -> list[PolicyFinding]:
|
|
532
|
+
return [
|
|
533
|
+
finding
|
|
534
|
+
for doc, text in documents.items()
|
|
535
|
+
for finding in check_document(doc, text, pack)
|
|
536
|
+
]
|