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/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
+ ]