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/audit.py ADDED
@@ -0,0 +1,1120 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import posixpath
6
+ import re
7
+ import subprocess
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from urllib.parse import unquote
11
+
12
+ from clean_docs.applicability import (
13
+ DocumentProfile,
14
+ classify_document,
15
+ frontmatter_error,
16
+ role_override_error,
17
+ )
18
+ from clean_docs.corpus import _git_visible_markdown, _is_document_candidate, scan_corpus
19
+ from clean_docs.errors import ConfigurationError
20
+ from clean_docs.mdx import (
21
+ MdxDocument,
22
+ MdxParserError,
23
+ parse_mdx_documents,
24
+ parser_availability,
25
+ )
26
+ from clean_docs.policy import REGISTER_PROFILE, check_document
27
+ from clean_docs.regions import atomic_write
28
+ from clean_docs.residue import scan_residue
29
+ from clean_docs.standard import load_default_pack
30
+
31
+
32
+ LINK = re.compile(
33
+ r"\[[^\]]+\]\(\s*(<[^>\n]*>|\[[^\]\n]+\]|\{[^}\n]+\}|[^)\s]+)"
34
+ )
35
+ HEADING = re.compile(r"^#{2,}\s+(.+?)\s*$")
36
+ IDENTITY_HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*$")
37
+ PURPOSE_BLOCK = re.compile(
38
+ r"<!-- sourcebound:purpose -->\s*(.*?)\s*<!-- sourcebound:end purpose -->",
39
+ re.DOTALL,
40
+ )
41
+ STOCK_PURPOSE_OPENING = re.compile(
42
+ r"^Use this (?:guide|model|page|path|policy|reference|specification) when\b",
43
+ re.IGNORECASE,
44
+ )
45
+ ALLOW = re.compile(
46
+ r'<!--\s*sourcebound:allow\s+([a-z][a-z-]+)\s+reason="([^"]+)"\s*-->'
47
+ )
48
+ AUDIT_BASELINE_SCHEMA_V1 = "sourcebound.audit-baseline.v1"
49
+ AUDIT_BASELINE_SCHEMA = "sourcebound.audit-baseline.v2"
50
+ AUDIT_BASELINE_PATH = Path(".sourcebound/audit-baseline.json")
51
+
52
+
53
+ def _is_test_fixture_path(value: str) -> bool:
54
+ path = Path(value)
55
+ parts = set(path.parts)
56
+ name = path.name
57
+ return bool(
58
+ parts & {"test", "tests", "__tests__", "fixtures", "__fixtures__"}
59
+ or ".test." in name
60
+ or ".spec." in name
61
+ )
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class AuditFinding:
66
+ rule: str
67
+ path: str
68
+ line: int
69
+ detail: str
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class AuditReport:
74
+ documents: tuple[str, ...]
75
+ ignored_documents: tuple[str, ...]
76
+ findings: tuple[AuditFinding, ...]
77
+ baselined_findings: tuple[AuditFinding, ...] = ()
78
+ stale_baseline: tuple[AuditFinding, ...] = ()
79
+ unsupported_documents: tuple[str, ...] = ()
80
+ advisories: tuple[AuditFinding, ...] = ()
81
+ advisory_totals: tuple[tuple[str, int], ...] = ()
82
+ document_profiles: tuple[DocumentProfile, ...] = ()
83
+ repository_integrity_enforced: bool = False
84
+ policy_preview: bool = False
85
+
86
+ @property
87
+ def ok(self) -> bool:
88
+ return not self.findings and not self.stale_baseline
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class _BaselineIdentity:
93
+ finding: AuditFinding
94
+ normalized: str
95
+ section_anchor: str
96
+ duplicate_ordinal: int
97
+ fingerprint: str
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class _LoadedBaseline:
102
+ schema: str
103
+ identities: tuple[_BaselineIdentity, ...]
104
+
105
+
106
+ def _normalized_finding_content(finding: AuditFinding) -> str:
107
+ return " ".join(finding.detail.split())
108
+
109
+
110
+ def finding_fingerprint(
111
+ finding: AuditFinding,
112
+ *,
113
+ section_anchor: str = "__document__",
114
+ duplicate_ordinal: int = 1,
115
+ ) -> str:
116
+ payload = json.dumps(
117
+ {
118
+ "duplicate_ordinal": duplicate_ordinal,
119
+ "normalized": _normalized_finding_content(finding),
120
+ "path": finding.path,
121
+ "rule": finding.rule,
122
+ "section_anchor": section_anchor,
123
+ },
124
+ sort_keys=True,
125
+ separators=(",", ":"),
126
+ )
127
+ return hashlib.sha256(payload.encode()).hexdigest()
128
+
129
+
130
+ def _finding_order(finding: AuditFinding) -> tuple[str, int, str, str]:
131
+ return (finding.path, finding.line, finding.rule, finding.detail)
132
+
133
+
134
+ def _legacy_finding_fingerprint(finding: AuditFinding) -> str:
135
+ payload = json.dumps(
136
+ {
137
+ "detail": finding.detail,
138
+ "line": finding.line,
139
+ "path": finding.path,
140
+ "rule": finding.rule,
141
+ },
142
+ sort_keys=True,
143
+ separators=(",", ":"),
144
+ )
145
+ return hashlib.sha256(payload.encode()).hexdigest()
146
+
147
+
148
+ def _section_anchor(root: Path | None, finding: AuditFinding) -> str:
149
+ if root is None:
150
+ return "__document__"
151
+ try:
152
+ text = (root / finding.path).read_text(encoding="utf-8")
153
+ if Path(finding.path).suffix.lower() == ".mdx":
154
+ text = parse_mdx_documents({finding.path: text})[0][
155
+ finding.path
156
+ ].policy_text(text)
157
+ lines = text.splitlines()
158
+ except (OSError, MdxParserError, KeyError):
159
+ return "__document__"
160
+ anchor = "__document__"
161
+ for line in lines[: finding.line]:
162
+ if match := IDENTITY_HEADING.match(line):
163
+ anchor = re.sub(
164
+ r"-+",
165
+ "-",
166
+ re.sub(r"[^a-z0-9]+", "-", match.group(1).lower()),
167
+ ).strip("-") or "__document__"
168
+ return anchor
169
+
170
+
171
+ def _baseline_identities(
172
+ findings: tuple[AuditFinding, ...],
173
+ *,
174
+ root: Path | None,
175
+ ) -> tuple[_BaselineIdentity, ...]:
176
+ prepared = [
177
+ (
178
+ finding,
179
+ _normalized_finding_content(finding),
180
+ _section_anchor(root, finding),
181
+ )
182
+ for finding in findings
183
+ ]
184
+ prepared.sort(
185
+ key=lambda item: (
186
+ item[0].path,
187
+ item[0].rule,
188
+ item[1],
189
+ item[2],
190
+ item[0].line,
191
+ item[0].detail,
192
+ )
193
+ )
194
+ counts: dict[tuple[str, str, str, str], int] = {}
195
+ identities: list[_BaselineIdentity] = []
196
+ for finding, normalized, section_anchor in prepared:
197
+ key = (finding.path, finding.rule, normalized, section_anchor)
198
+ duplicate_ordinal = counts.get(key, 0) + 1
199
+ counts[key] = duplicate_ordinal
200
+ identities.append(
201
+ _BaselineIdentity(
202
+ finding,
203
+ normalized,
204
+ section_anchor,
205
+ duplicate_ordinal,
206
+ finding_fingerprint(
207
+ finding,
208
+ section_anchor=section_anchor,
209
+ duplicate_ordinal=duplicate_ordinal,
210
+ ),
211
+ )
212
+ )
213
+ return tuple(identities)
214
+
215
+
216
+ def _bounded_advisories(
217
+ findings: list[AuditFinding],
218
+ *,
219
+ per_rule: int = 3,
220
+ ) -> tuple[tuple[AuditFinding, ...], tuple[tuple[str, int], ...]]:
221
+ totals: dict[str, int] = {}
222
+ selected: list[AuditFinding] = []
223
+ emitted: dict[str, int] = {}
224
+ for finding in sorted(findings, key=_finding_order):
225
+ totals[finding.rule] = totals.get(finding.rule, 0) + 1
226
+ count = emitted.get(finding.rule, 0)
227
+ if count < per_rule:
228
+ selected.append(finding)
229
+ emitted[finding.rule] = count + 1
230
+ return tuple(selected), tuple(sorted(totals.items()))
231
+
232
+
233
+ def render_audit_baseline(
234
+ findings: tuple[AuditFinding, ...],
235
+ *,
236
+ root: Path | None = None,
237
+ ) -> str:
238
+ entries = []
239
+ for identity in _baseline_identities(findings, root=root):
240
+ finding = identity.finding
241
+ entries.append({
242
+ "fingerprint": identity.fingerprint,
243
+ "rule": finding.rule,
244
+ "path": finding.path,
245
+ "line_hint": finding.line,
246
+ "detail": finding.detail,
247
+ "normalized": identity.normalized,
248
+ "section_anchor": identity.section_anchor,
249
+ "duplicate_ordinal": identity.duplicate_ordinal,
250
+ })
251
+ return json.dumps(
252
+ {"schema": AUDIT_BASELINE_SCHEMA, "findings": entries},
253
+ indent=2,
254
+ ) + "\n"
255
+
256
+
257
+ def _load_audit_baseline(path: Path) -> _LoadedBaseline:
258
+ if path.is_symlink():
259
+ raise ConfigurationError(f"audit baseline cannot be a symbolic link: {path}")
260
+ try:
261
+ raw = json.loads(path.read_text(encoding="utf-8"))
262
+ except (OSError, json.JSONDecodeError) as exc:
263
+ raise ConfigurationError(f"cannot read audit baseline {path}: {exc}") from exc
264
+ if not isinstance(raw, dict) or raw.get("schema") not in {
265
+ AUDIT_BASELINE_SCHEMA_V1,
266
+ AUDIT_BASELINE_SCHEMA,
267
+ }:
268
+ raise ConfigurationError(f"audit baseline has an unsupported schema: {path}")
269
+ schema = raw["schema"]
270
+ entries = raw.get("findings")
271
+ if not isinstance(entries, list):
272
+ raise ConfigurationError(f"audit baseline findings must be a list: {path}")
273
+ identities: list[_BaselineIdentity] = []
274
+ fingerprints: set[str] = set()
275
+ identity_keys: set[tuple[str, str, str, str, int]] = set()
276
+ for index, entry in enumerate(entries):
277
+ if not isinstance(entry, dict):
278
+ raise ConfigurationError(f"audit baseline finding {index} must be an object")
279
+ expected_keys = (
280
+ {"fingerprint", "rule", "path", "line", "detail"}
281
+ if schema == AUDIT_BASELINE_SCHEMA_V1
282
+ else {
283
+ "fingerprint",
284
+ "rule",
285
+ "path",
286
+ "line_hint",
287
+ "detail",
288
+ "normalized",
289
+ "section_anchor",
290
+ "duplicate_ordinal",
291
+ }
292
+ )
293
+ if set(entry) != expected_keys:
294
+ raise ConfigurationError(
295
+ f"audit baseline finding {index} has fields that do not match {schema}"
296
+ )
297
+ if not all(
298
+ isinstance(entry[key], str)
299
+ for key in ("fingerprint", "rule", "path", "detail")
300
+ ):
301
+ raise ConfigurationError(f"audit baseline finding {index} has an invalid string field")
302
+ line_key = "line" if schema == AUDIT_BASELINE_SCHEMA_V1 else "line_hint"
303
+ if (
304
+ not isinstance(entry[line_key], int)
305
+ or isinstance(entry[line_key], bool)
306
+ or entry[line_key] < 1
307
+ ):
308
+ raise ConfigurationError(f"audit baseline finding {index} has an invalid line")
309
+ finding = AuditFinding(
310
+ entry["rule"],
311
+ entry["path"],
312
+ entry[line_key],
313
+ entry["detail"],
314
+ )
315
+ if schema == AUDIT_BASELINE_SCHEMA_V1:
316
+ normalized = _normalized_finding_content(finding)
317
+ section_anchor = "__legacy_line__"
318
+ duplicate_ordinal = 1
319
+ fingerprint = _legacy_finding_fingerprint(finding)
320
+ else:
321
+ if not all(
322
+ isinstance(entry[key], str)
323
+ for key in ("normalized", "section_anchor")
324
+ ):
325
+ raise ConfigurationError(
326
+ f"audit baseline finding {index} has invalid identity material"
327
+ )
328
+ duplicate_ordinal = entry["duplicate_ordinal"]
329
+ if (
330
+ not isinstance(duplicate_ordinal, int)
331
+ or isinstance(duplicate_ordinal, bool)
332
+ or duplicate_ordinal < 1
333
+ ):
334
+ raise ConfigurationError(
335
+ f"audit baseline finding {index} has an invalid duplicate ordinal"
336
+ )
337
+ normalized = entry["normalized"]
338
+ section_anchor = entry["section_anchor"]
339
+ if normalized != _normalized_finding_content(finding):
340
+ raise ConfigurationError(
341
+ f"audit baseline finding {index} normalized content does not match"
342
+ )
343
+ fingerprint = finding_fingerprint(
344
+ finding,
345
+ section_anchor=section_anchor,
346
+ duplicate_ordinal=duplicate_ordinal,
347
+ )
348
+ if entry["fingerprint"] != fingerprint:
349
+ raise ConfigurationError(f"audit baseline finding {index} fingerprint does not match")
350
+ if fingerprint in fingerprints:
351
+ raise ConfigurationError(f"audit baseline has duplicate finding {fingerprint}")
352
+ identity_key = (
353
+ finding.path,
354
+ finding.rule,
355
+ normalized,
356
+ section_anchor,
357
+ duplicate_ordinal,
358
+ )
359
+ if schema == AUDIT_BASELINE_SCHEMA and identity_key in identity_keys:
360
+ raise ConfigurationError(
361
+ f"audit baseline has duplicate identity at finding {index}"
362
+ )
363
+ fingerprints.add(fingerprint)
364
+ identity_keys.add(identity_key)
365
+ identities.append(
366
+ _BaselineIdentity(
367
+ finding,
368
+ normalized,
369
+ section_anchor,
370
+ duplicate_ordinal,
371
+ fingerprint,
372
+ )
373
+ )
374
+ identities.sort(key=lambda item: item.fingerprint)
375
+ return _LoadedBaseline(schema, tuple(identities))
376
+
377
+
378
+ def _tracked_markdown(root: Path) -> list[Path]:
379
+ visible = _git_visible_markdown(root)
380
+ if visible is not None:
381
+ return [
382
+ relative
383
+ for path in visible
384
+ if _is_document_candidate(
385
+ relative := path.relative_to(root),
386
+ fallback=False,
387
+ )
388
+ ]
389
+ return sorted(
390
+ relative
391
+ for pattern in ("*.md", "*.mdx")
392
+ for path in root.rglob(pattern)
393
+ if _is_document_candidate(
394
+ relative := path.relative_to(root),
395
+ fallback=True,
396
+ )
397
+ )
398
+
399
+
400
+ def _repository_entries(root: Path) -> set[str]:
401
+ try:
402
+ proc = subprocess.run(
403
+ [
404
+ "git",
405
+ "-C",
406
+ str(root),
407
+ "ls-files",
408
+ "--cached",
409
+ "--others",
410
+ "--exclude-standard",
411
+ "-z",
412
+ ],
413
+ capture_output=True,
414
+ timeout=20,
415
+ check=False,
416
+ )
417
+ except (subprocess.SubprocessError, OSError):
418
+ proc = None
419
+ if proc is not None and proc.returncode == 0:
420
+ return {
421
+ path
422
+ for path in proc.stdout.decode(errors="surrogateescape").split("\0")
423
+ if path
424
+ }
425
+ return {
426
+ path.relative_to(root).as_posix()
427
+ for path in root.rglob("*")
428
+ if path.is_file()
429
+ }
430
+
431
+
432
+ def _hidden_document(relative: Path) -> bool:
433
+ hidden = [part for part in relative.parts if part.startswith(".")]
434
+ return bool(hidden) and relative.parts[0] != ".agents"
435
+
436
+
437
+ def _allowances(lines: list[str]) -> set[str]:
438
+ allowed: set[str] = set()
439
+ for line in lines:
440
+ match = ALLOW.search(line)
441
+ if match and len(match.group(2).strip()) >= 12:
442
+ allowed.add(match.group(1))
443
+ return allowed
444
+
445
+
446
+ def _allowance_records(lines: list[str]) -> list[tuple[int, str, str]]:
447
+ records: list[tuple[int, str, str]] = []
448
+ for line_number, line in enumerate(lines, start=1):
449
+ if match := ALLOW.search(line):
450
+ records.append((line_number, match.group(1), match.group(2).strip()))
451
+ return records
452
+
453
+
454
+ def _section_ranges(lines: list[str]) -> list[tuple[str, int, int, set[str]]]:
455
+ headings = [
456
+ (index, match.group(1))
457
+ for index, line in enumerate(lines)
458
+ if (match := HEADING.match(line))
459
+ ]
460
+ sections = []
461
+ for position, (start, title) in enumerate(headings):
462
+ end = headings[position + 1][0] if position + 1 < len(headings) else len(lines)
463
+ sections.append((title, start + 1, end - start, _allowances(lines[start:end])))
464
+ return sections
465
+
466
+
467
+ def _local_link(target: str) -> bool:
468
+ return not target.startswith(("#", "http://", "https://", "mailto:", "data:"))
469
+
470
+
471
+ def _mask_inline_code(line: str) -> str:
472
+ result = list(line)
473
+ index = 0
474
+ while index < len(line):
475
+ if line[index] != "`" or (index > 0 and line[index - 1] == "\\"):
476
+ index += 1
477
+ continue
478
+ width = 1
479
+ while index + width < len(line) and line[index + width] == "`":
480
+ width += 1
481
+ end = line.find("`" * width, index + width)
482
+ if end == -1:
483
+ index += width
484
+ continue
485
+ for position in range(index, end + width):
486
+ result[position] = " "
487
+ index = end + width
488
+ return "".join(result)
489
+
490
+
491
+ def _markdown_links(lines: list[str]) -> list[tuple[int, str]]:
492
+ links: list[tuple[int, str]] = []
493
+ fence: tuple[str, int] | None = None
494
+ for line_number, line in enumerate(lines, start=1):
495
+ fence_match = re.match(r"^\s{0,3}(`{3,}|~{3,})", line)
496
+ if fence_match:
497
+ marker = fence_match.group(1)
498
+ if fence is None:
499
+ fence = (marker[0], len(marker))
500
+ elif marker[0] == fence[0] and len(marker) >= fence[1]:
501
+ fence = None
502
+ continue
503
+ if fence is not None:
504
+ continue
505
+ visible = _mask_inline_code(line)
506
+ for match in LINK.finditer(visible):
507
+ links.append((line_number, match.group(1)))
508
+ return links
509
+
510
+
511
+ def _placeholder_link_target(target: str) -> bool:
512
+ candidate = target.strip()
513
+ if candidate in {"...", "…"} or "…" in candidate:
514
+ return True
515
+ if re.search(r"<(?:[A-Za-z][A-Za-z0-9_-]*)>", candidate):
516
+ return True
517
+ if re.search(r"\{(?:[A-Za-z][A-Za-z0-9_-]*)}", candidate):
518
+ return True
519
+ if (
520
+ candidate.startswith("[")
521
+ and candidate.endswith("]")
522
+ and re.search(r"\s", candidate[1:-1])
523
+ ):
524
+ return True
525
+ if (
526
+ candidate.startswith("<")
527
+ and candidate.endswith(">")
528
+ and re.search(r"\s", candidate[1:-1])
529
+ ):
530
+ return True
531
+ return False
532
+
533
+
534
+ def _entry_exists(entries: set[str], candidate: str) -> bool:
535
+ normalized = candidate.rstrip("/")
536
+ return normalized in entries or any(
537
+ entry.startswith(normalized + "/") for entry in entries
538
+ )
539
+
540
+
541
+ def _link_target_exists(
542
+ root: Path,
543
+ source: Path,
544
+ raw_target: str,
545
+ entries: set[str],
546
+ ) -> bool:
547
+ target = unquote(raw_target.split("#", 1)[0].split("?", 1)[0]).strip()
548
+ if target.startswith("<") and target.endswith(">"):
549
+ target = target[1:-1]
550
+ if not target or not _local_link(target):
551
+ return True
552
+ repository_root = target.startswith("/")
553
+ if repository_root:
554
+ candidate = posixpath.normpath(target.lstrip("/"))
555
+ else:
556
+ candidate = posixpath.normpath(
557
+ posixpath.join(source.parent.as_posix(), target)
558
+ )
559
+ if candidate == ".." or candidate.startswith("../"):
560
+ return False
561
+ candidates = [candidate]
562
+ if not Path(candidate).suffix:
563
+ candidates.extend(
564
+ (
565
+ candidate + ".md",
566
+ candidate + ".mdx",
567
+ posixpath.join(candidate, "README.md"),
568
+ posixpath.join(candidate, "index.md"),
569
+ posixpath.join(candidate, "index.mdx"),
570
+ )
571
+ )
572
+ if any(_entry_exists(entries, item) for item in candidates):
573
+ return True
574
+ if any((root / item).exists() for item in candidates):
575
+ return True
576
+ # A leading slash can address an application or publication mount. Without
577
+ # a declared mount, treating it as a repository path creates false blockers.
578
+ return repository_root
579
+
580
+
581
+ def _outside_fences(lines: list[str]) -> list[tuple[int, str]]:
582
+ result: list[tuple[int, str]] = []
583
+ in_fence = False
584
+ for line_number, line in enumerate(lines, start=1):
585
+ if line.startswith("```"):
586
+ in_fence = not in_fence
587
+ continue
588
+ if not in_fence:
589
+ result.append((line_number, line))
590
+ return result
591
+
592
+
593
+ def _page_type(relative: Path, text: str) -> str:
594
+ name = relative.name.upper()
595
+ if relative.name == "README.md":
596
+ return "readme"
597
+ if (
598
+ "REFERENCE" in name
599
+ or name in {
600
+ "STANDARD.MD",
601
+ "SOURCEBOUND_SPEC.MD",
602
+ "DECISION_LOG.MD",
603
+ "CLI.MD",
604
+ "RELEASES.MD",
605
+ "SURFACE.MD",
606
+ }
607
+ or text.lstrip().lower().startswith("# reference")
608
+ ):
609
+ return "reference"
610
+ return "guide"
611
+
612
+
613
+ def _section_depth_findings(
614
+ normalized: str,
615
+ lines: list[str],
616
+ *,
617
+ require_routes: bool,
618
+ require_depth_links: bool,
619
+ ) -> list[AuditFinding]:
620
+ findings: list[AuditFinding] = []
621
+ text = "\n".join(lines)
622
+ allowances = _allowances(lines)
623
+ if (
624
+ require_routes
625
+ and normalized == "README.md"
626
+ and "readme-routing" not in allowances
627
+ ):
628
+ if not all(
629
+ marker in text
630
+ for marker in ("If you need to", "Start with", "You will leave with")
631
+ ):
632
+ findings.append(AuditFinding(
633
+ "readme-routing",
634
+ normalized,
635
+ 1,
636
+ "add the required job-to-page routing table near the first action",
637
+ ))
638
+ in_fence = False
639
+ fence_start = 0
640
+ for line_number, line in enumerate(lines, start=1):
641
+ if line.startswith("```"):
642
+ if in_fence and line_number - fence_start > 13:
643
+ findings.append(AuditFinding(
644
+ "readme-reference-depth",
645
+ normalized,
646
+ fence_start,
647
+ "move configuration or schema examples over 12 lines to a reference page",
648
+ ))
649
+ else:
650
+ fence_start = line_number
651
+ in_fence = not in_fence
652
+ table_start = 0
653
+ table_lines = 0
654
+ for line_number, line in enumerate([*lines, ""], start=1):
655
+ if line.startswith("|"):
656
+ if table_lines == 0:
657
+ table_start = line_number
658
+ table_lines += 1
659
+ else:
660
+ if table_lines > 14:
661
+ findings.append(AuditFinding(
662
+ "readme-reference-depth",
663
+ normalized,
664
+ table_start,
665
+ "move lookup tables over 12 data rows to a reference page",
666
+ ))
667
+ table_lines = 0
668
+ if (
669
+ require_depth_links
670
+ and "elaboration-depth" not in allowances
671
+ and (normalized == "README.md" or normalized.startswith("docs/learn/"))
672
+ ):
673
+ for title, section_line, _count, _allowances_for_section in _section_ranges(lines):
674
+ start = section_line - 1
675
+ following = [
676
+ index
677
+ for index, line in enumerate(lines[start + 1:], start=start + 1)
678
+ if HEADING.match(line)
679
+ ]
680
+ end = following[0] if following else len(lines)
681
+ body = "\n".join(lines[start + 1:end])
682
+ words = re.findall(r"\b[\w'-]+\b", re.sub(r"`[^`]+`", "", body))
683
+ deeper_link = any(
684
+ target.split("#", 1)[0].endswith((".md", "/"))
685
+ for target in LINK.findall(body)
686
+ )
687
+ if len(words) > 80 and not deeper_link:
688
+ findings.append(AuditFinding(
689
+ "elaboration-depth",
690
+ normalized,
691
+ section_line,
692
+ f"{title!r} has {len(words)} words without a deeper-page route",
693
+ ))
694
+ return findings
695
+
696
+
697
+ def _assurance_findings(documents: dict[str, str]) -> list[AuditFinding]:
698
+ rules = (
699
+ (
700
+ "model-authority",
701
+ "docs/learn/deep-dive-the-deterministic-seam.md",
702
+ re.compile(
703
+ r"(?:deterministic code.{0,80}(?:owns|decides)|"
704
+ r"models? may select|models?.{0,80}(?:cannot|never).{0,40}gate)",
705
+ re.I,
706
+ ),
707
+ ),
708
+ (
709
+ "execution-boundary",
710
+ "docs/SECURITY_MODEL.md",
711
+ re.compile(
712
+ r"(?:does not (?:execute|revoke)|without (?:importing|executing) "
713
+ r"(?:repository|target) code)",
714
+ re.I,
715
+ ),
716
+ ),
717
+ )
718
+ findings: list[AuditFinding] = []
719
+ for _boundary, canonical, pattern in rules:
720
+ for doc, text in documents.items():
721
+ if (
722
+ doc == canonical
723
+ or REGISTER_PROFILE not in text
724
+ or "assurance-dedup" in _allowances(text.splitlines())
725
+ ):
726
+ continue
727
+ for line_number, line in _outside_fences(text.splitlines()):
728
+ if pattern.search(line):
729
+ findings.append(AuditFinding(
730
+ "assurance-dedup",
731
+ doc,
732
+ line_number,
733
+ f"link to {canonical} instead of restating this authority boundary",
734
+ ))
735
+ return findings
736
+
737
+
738
+ def _purpose_template_findings(documents: dict[str, str]) -> list[AuditFinding]:
739
+ matches: list[str] = []
740
+ for doc, text in documents.items():
741
+ if REGISTER_PROFILE not in text:
742
+ continue
743
+ block = PURPOSE_BLOCK.search(text)
744
+ if block and STOCK_PURPOSE_OPENING.search(" ".join(block.group(1).split())):
745
+ matches.append(doc)
746
+ if len(matches) < 3:
747
+ return []
748
+ return [
749
+ AuditFinding(
750
+ "purpose-template",
751
+ doc,
752
+ next(
753
+ (
754
+ line_number + 1
755
+ for line_number, line in enumerate(
756
+ documents[doc].splitlines(), start=1
757
+ )
758
+ if line.strip() == "<!-- sourcebound:purpose -->"
759
+ ),
760
+ 1,
761
+ ),
762
+ "replace the repeated 'Use this ... when' shell with a reader situation specific to this page",
763
+ )
764
+ for doc in matches
765
+ ]
766
+
767
+
768
+ def _scan_audit(root: Path, *, preview_policy: bool = False) -> AuditReport:
769
+ root = root.resolve()
770
+ repository_integrity_enforced = (root / ".sourcebound.yml").is_file()
771
+ pack = load_default_pack()
772
+ repository_entries = _repository_entries(root)
773
+ tracked_documents = _tracked_markdown(root)
774
+ mdx_sources: dict[str, str] = {}
775
+ mdx_failures: dict[str, str] = {}
776
+ parsed_mdx: dict[str, MdxDocument] = {}
777
+ for relative in tracked_documents:
778
+ if relative.suffix.lower() != ".mdx":
779
+ continue
780
+ normalized = relative.as_posix()
781
+ try:
782
+ mdx_sources[normalized] = (root / relative).read_text(encoding="utf-8")
783
+ except (OSError, UnicodeError) as exc:
784
+ mdx_failures[normalized] = f"cannot read MDX: {exc}"
785
+ if mdx_sources:
786
+ available, detail = parser_availability()
787
+ if not available:
788
+ mdx_failures.update(
789
+ (path, detail) for path in mdx_sources
790
+ )
791
+ else:
792
+ try:
793
+ parsed_mdx, parser_failures = parse_mdx_documents(mdx_sources)
794
+ except MdxParserError as exc:
795
+ parser_failures = {
796
+ path: str(exc) for path in mdx_sources
797
+ }
798
+ mdx_failures.update(parser_failures)
799
+ unsupported: set[str] = set()
800
+ section_limit = int(pack["policy"]["section_max_lines"])
801
+ active: list[str] = []
802
+ ignored: list[str] = []
803
+ active_texts: dict[str, str] = {}
804
+ profiles: dict[str, DocumentProfile] = {}
805
+ invalid_roles: set[str] = set()
806
+ findings: list[AuditFinding] = []
807
+ advisories: list[AuditFinding] = []
808
+ for relative in tracked_documents:
809
+ normalized = relative.as_posix()
810
+ if "archive" in relative.parts or _hidden_document(relative):
811
+ ignored.append(normalized)
812
+ continue
813
+ path = root / relative
814
+ try:
815
+ text = (
816
+ mdx_sources[normalized]
817
+ if relative.suffix.lower() == ".mdx"
818
+ else path.read_text(encoding="utf-8")
819
+ )
820
+ except (OSError, UnicodeError) as exc:
821
+ candidate = AuditFinding("unreadable-document", normalized, 1, str(exc))
822
+ (findings if repository_integrity_enforced else advisories).append(candidate)
823
+ continue
824
+ mdx_document = parsed_mdx.get(normalized)
825
+ if relative.suffix.lower() == ".mdx" and mdx_document is None:
826
+ unsupported.add(normalized)
827
+ candidate = AuditFinding(
828
+ "unsupported-mdx",
829
+ normalized,
830
+ 1,
831
+ mdx_failures.get(normalized, "MDX parser did not return a result"),
832
+ )
833
+ (findings if repository_integrity_enforced else advisories).append(candidate)
834
+ continue
835
+ active.append(normalized)
836
+ lines = text.splitlines()
837
+ policy_text = (
838
+ mdx_document.policy_text(text) if mdx_document is not None else text
839
+ )
840
+ policy_lines = policy_text.splitlines()
841
+ active_texts[normalized] = policy_text
842
+ profile = classify_document(relative, policy_text)
843
+ profiles[normalized] = profile
844
+ role_error = role_override_error(text)
845
+ structure_error = frontmatter_error(text)
846
+ if role_error or structure_error:
847
+ invalid_roles.add(normalized)
848
+ findings.append(AuditFinding(
849
+ (
850
+ "invalid-document-role"
851
+ if role_error
852
+ else "malformed-frontmatter"
853
+ ),
854
+ normalized,
855
+ 1,
856
+ role_error or structure_error or "invalid document structure",
857
+ ))
858
+ allowances = _allowances(policy_lines)
859
+ evaluate_policy = profile.registered or preview_policy
860
+ if preview_policy and not profile.registered:
861
+ policy_text = f"{policy_text.rstrip()}\n\n{REGISTER_PROFILE}\n"
862
+ for item in (
863
+ ()
864
+ if role_error or not evaluate_policy
865
+ else check_document(
866
+ normalized,
867
+ policy_text,
868
+ pack,
869
+ semantic_text=text,
870
+ )
871
+ ):
872
+ if not profile.applies(item.rule):
873
+ continue
874
+ candidate = AuditFinding(item.rule, item.doc, item.line, item.detail)
875
+ (findings if profile.registered else advisories).append(candidate)
876
+ page_type = _page_type(relative, text)
877
+ if (
878
+ not role_error
879
+ and evaluate_policy
880
+ and profile.role in {"overview", "task", "tutorial"}
881
+ ):
882
+ doc_limit = (
883
+ int(pack["policy"]["readme_max_lines"])
884
+ if page_type == "readme"
885
+ else int(pack["policy"]["guide_max_lines"])
886
+ )
887
+ for allowance_line, rule, reason in _allowance_records(policy_lines):
888
+ if rule in {"doc-length", "section-length"} and not re.search(
889
+ r"\b(?:cut|moved|split|linked|reference)\b", reason, re.I
890
+ ):
891
+ candidate = AuditFinding(
892
+ "invalid-length-allowance",
893
+ normalized,
894
+ allowance_line,
895
+ "replace comprehensiveness rationale with a subtraction receipt",
896
+ )
897
+ advisories.append(candidate)
898
+ if (
899
+ len(lines) > doc_limit
900
+ and "doc-length" not in allowances
901
+ ):
902
+ candidate = AuditFinding(
903
+ "doc-length",
904
+ normalized,
905
+ 1,
906
+ f"{len(lines)} lines exceeds the {page_type} budget of {doc_limit}; move a second job behind a link",
907
+ )
908
+ advisories.append(candidate)
909
+ for title, section_line, count, section_allowances in _section_ranges(
910
+ policy_lines
911
+ ):
912
+ if count > section_limit and "section-length" not in section_allowances:
913
+ candidate = AuditFinding(
914
+ "section-length",
915
+ normalized,
916
+ section_line,
917
+ f"{title!r} is {count} lines; move its second job behind a link",
918
+ )
919
+ advisories.append(candidate)
920
+ for candidate in _section_depth_findings(
921
+ normalized,
922
+ policy_lines,
923
+ require_routes=bool(pack["policy"].get("require_readme_routes")),
924
+ require_depth_links=bool(pack["policy"].get("require_depth_links")),
925
+ ):
926
+ if (
927
+ not role_error
928
+ and evaluate_policy
929
+ and profile.applies(candidate.rule)
930
+ ):
931
+ (findings if profile.registered else advisories).append(candidate)
932
+ document_links = (
933
+ [(link.line, link.url) for link in mdx_document.links]
934
+ if mdx_document is not None
935
+ else _markdown_links(lines)
936
+ )
937
+ for line_number, target in document_links:
938
+ if (
939
+ _placeholder_link_target(target)
940
+ and profile.role in {"agent-procedure", "template"}
941
+ ):
942
+ advisories.append(
943
+ AuditFinding(
944
+ "placeholder-link",
945
+ normalized,
946
+ line_number,
947
+ f"template destination is unresolved: {target}",
948
+ )
949
+ )
950
+ continue
951
+ if not _link_target_exists(root, relative, target, repository_entries):
952
+ candidate = AuditFinding(
953
+ "broken-local-link",
954
+ normalized,
955
+ line_number,
956
+ f"target does not exist: {target}",
957
+ )
958
+ (
959
+ findings
960
+ if repository_integrity_enforced or profile.registered
961
+ else advisories
962
+ ).append(candidate)
963
+ # These comparisons require editorial ownership knowledge. They remain
964
+ # visible, but they cannot reject a repository from token overlap alone.
965
+ advisories.extend(_assurance_findings(active_texts))
966
+ for candidate in _purpose_template_findings(active_texts):
967
+ if candidate.path in invalid_roles:
968
+ continue
969
+ scoped_profile = profiles.get(candidate.path)
970
+ if scoped_profile is None or not scoped_profile.applies(candidate.rule):
971
+ continue
972
+ (findings if scoped_profile.registered else advisories).append(candidate)
973
+ corpus_rule_names = {
974
+ "surface": "process-artifact",
975
+ "audience": "audience",
976
+ "provenance": "provenance",
977
+ "near-dup": "near-duplicate",
978
+ "restatement": "restatement",
979
+ }
980
+ for corpus_finding in scan_corpus(
981
+ root,
982
+ include_lengths=False,
983
+ prepared_documents=active_texts,
984
+ ):
985
+ corpus_rule = corpus_rule_names.get(corpus_finding.rule)
986
+ if corpus_rule is None:
987
+ continue
988
+ corpus_profile = profiles.get(corpus_finding.doc)
989
+ if corpus_profile is None:
990
+ continue
991
+ if corpus_rule == "audience" and corpus_profile.role in {
992
+ "agent-procedure",
993
+ "template",
994
+ }:
995
+ continue
996
+ if corpus_rule == "provenance" and corpus_profile.role in {"evidence", "plan"}:
997
+ continue
998
+ if corpus_rule in {"near-duplicate", "restatement"}:
999
+ if corpus_profile.role in {"agent-procedure", "evidence", "plan", "template"}:
1000
+ continue
1001
+ counterpart = re.search(r"overlap with (.+):\d+", corpus_finding.detail)
1002
+ if counterpart:
1003
+ other = profiles.get(counterpart.group(1))
1004
+ if other is not None and other.role != corpus_profile.role:
1005
+ continue
1006
+ corpus_text = active_texts.get(corpus_finding.doc)
1007
+ if corpus_text is None:
1008
+ continue
1009
+ if corpus_rule in _allowances(corpus_text.splitlines()):
1010
+ continue
1011
+ candidate = AuditFinding(
1012
+ corpus_rule,
1013
+ corpus_finding.doc,
1014
+ corpus_finding.line,
1015
+ corpus_finding.detail,
1016
+ )
1017
+ if not any(
1018
+ existing.rule == candidate.rule
1019
+ and existing.path == candidate.path
1020
+ and existing.line == candidate.line
1021
+ for existing in [*findings, *advisories]
1022
+ ):
1023
+ advisories.append(candidate)
1024
+ for residue_finding in scan_residue(root):
1025
+ candidate = AuditFinding(
1026
+ residue_finding.rule,
1027
+ residue_finding.doc,
1028
+ residue_finding.line,
1029
+ residue_finding.detail,
1030
+ )
1031
+ residue_profile = profiles.get(residue_finding.doc)
1032
+ fixture_machine_path = (
1033
+ residue_finding.rule == "local-path-residue"
1034
+ and _is_test_fixture_path(residue_finding.doc)
1035
+ )
1036
+ residue_blocks = not fixture_machine_path and (
1037
+ repository_integrity_enforced
1038
+ or residue_profile is not None and residue_profile.registered
1039
+ )
1040
+ (
1041
+ findings
1042
+ if residue_blocks
1043
+ else advisories
1044
+ ).append(candidate)
1045
+ findings.sort(key=lambda item: (item.path, item.line, item.rule))
1046
+ bounded_advisories, advisory_totals = _bounded_advisories(advisories)
1047
+ return AuditReport(
1048
+ tuple(active),
1049
+ tuple(ignored),
1050
+ tuple(findings),
1051
+ unsupported_documents=tuple(sorted(unsupported)),
1052
+ advisories=bounded_advisories,
1053
+ advisory_totals=advisory_totals,
1054
+ document_profiles=tuple(
1055
+ profiles[path] for path in sorted(profiles)
1056
+ ),
1057
+ repository_integrity_enforced=repository_integrity_enforced,
1058
+ policy_preview=preview_policy,
1059
+ )
1060
+
1061
+
1062
+ def audit(
1063
+ root: Path,
1064
+ *,
1065
+ use_baseline: bool = True,
1066
+ preview_policy: bool = False,
1067
+ ) -> AuditReport:
1068
+ root = root.resolve()
1069
+ report = _scan_audit(root, preview_policy=preview_policy)
1070
+ baseline_path = root / AUDIT_BASELINE_PATH
1071
+ if not use_baseline or not baseline_path.exists():
1072
+ return report
1073
+ baseline = _load_audit_baseline(baseline_path)
1074
+ if baseline.schema == AUDIT_BASELINE_SCHEMA_V1:
1075
+ current = {
1076
+ _legacy_finding_fingerprint(item): item
1077
+ for item in report.findings
1078
+ }
1079
+ else:
1080
+ current = {
1081
+ identity.fingerprint: identity.finding
1082
+ for identity in _baseline_identities(report.findings, root=root)
1083
+ }
1084
+ recorded = {
1085
+ identity.fingerprint: identity.finding
1086
+ for identity in baseline.identities
1087
+ }
1088
+ matched = tuple(sorted(
1089
+ (current[fingerprint] for fingerprint in current.keys() & recorded.keys()),
1090
+ key=_finding_order,
1091
+ ))
1092
+ active = tuple(sorted(
1093
+ (current[fingerprint] for fingerprint in current.keys() - recorded.keys()),
1094
+ key=_finding_order,
1095
+ ))
1096
+ stale = tuple(sorted(
1097
+ (recorded[fingerprint] for fingerprint in recorded.keys() - current.keys()),
1098
+ key=_finding_order,
1099
+ ))
1100
+ return AuditReport(
1101
+ documents=report.documents,
1102
+ ignored_documents=report.ignored_documents,
1103
+ findings=active,
1104
+ baselined_findings=matched,
1105
+ stale_baseline=stale,
1106
+ unsupported_documents=report.unsupported_documents,
1107
+ advisories=report.advisories,
1108
+ advisory_totals=report.advisory_totals,
1109
+ document_profiles=report.document_profiles,
1110
+ repository_integrity_enforced=report.repository_integrity_enforced,
1111
+ policy_preview=report.policy_preview,
1112
+ )
1113
+
1114
+
1115
+ def write_audit_baseline(root: Path) -> Path:
1116
+ root = root.resolve()
1117
+ path = root / AUDIT_BASELINE_PATH
1118
+ raw_report = audit(root, use_baseline=False)
1119
+ atomic_write(path, render_audit_baseline(raw_report.findings, root=root))
1120
+ return path