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
@@ -0,0 +1,507 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path
10
+
11
+ from clean_docs.applicability import classify_document
12
+ from clean_docs.audit import AUDIT_BASELINE_PATH, audit, write_audit_baseline
13
+ from clean_docs.corpus import list_documents
14
+ from clean_docs.emit import render_llms_txt
15
+ from clean_docs.engine import evaluate
16
+ from clean_docs.errors import ConfigurationError, PolicyError
17
+ from clean_docs.extractors.inventory import INCLUDED_KINDS, extract_repository_overview
18
+ from clean_docs.inventory import InventoryItem, scan_inventory
19
+ from clean_docs.manifest import load_manifest
20
+ from clean_docs.models import (
21
+ LlmsTxtProjection,
22
+ Manifest,
23
+ ProjectionConfig,
24
+ RegionBinding,
25
+ Source,
26
+ )
27
+ from clean_docs.phrasing import GroundedDraft, ModelRecord, PhrasingProvider, build_model_record
28
+ from clean_docs.policy import REGISTER_PROFILE, ensure_purpose_contract
29
+ from clean_docs.projections import evaluate_projections
30
+ from clean_docs.regions import atomic_write, replace_region
31
+ from clean_docs.renderers import render
32
+ from clean_docs.snapshot import RepositorySnapshot
33
+ from clean_docs.write_gate import redact_secrets
34
+
35
+
36
+ REFERENCE_REGION = "repository-surface"
37
+ GENERATED_REFERENCE = ".sourcebound/repository-surface.md"
38
+ PLAN_FACT_LIMIT = 100
39
+ PLAN_DIFF_LIMIT = 4000
40
+ REFERENCE_SECTION = re.compile(
41
+ r"^## Repository surface\s*\n.*?(?=^## |\Z)", re.MULTILINE | re.DOTALL
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class PlannedWrite:
47
+ path: str
48
+ content: str
49
+ reason: str
50
+ diff: str
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class PlannedMove:
55
+ source: str
56
+ path: str
57
+ reason: str
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class BootstrapPlan:
62
+ facts: tuple[InventoryItem, ...]
63
+ writes: tuple[PlannedWrite, ...]
64
+ moves: tuple[PlannedMove, ...]
65
+ gaps: tuple[str, ...]
66
+ model: ModelRecord | None
67
+ digest: str
68
+ canonical_documents: tuple[str, ...] = ()
69
+ accept_hygiene_baseline: bool = False
70
+
71
+ def as_dict(self) -> dict[str, object]:
72
+ facts = []
73
+ for item in self.facts:
74
+ facts.append({
75
+ field: redact_secrets(value)[0] if isinstance(value, str) else value
76
+ for field, value in asdict(item).items()
77
+ })
78
+ serialized_facts = facts[:PLAN_FACT_LIMIT]
79
+ return {
80
+ "schema": "sourcebound.content-plan.v1",
81
+ "ok": not self.gaps,
82
+ "digest": self.digest,
83
+ "fact_count": len(facts),
84
+ "facts": serialized_facts,
85
+ "facts_omitted": len(facts) - len(serialized_facts),
86
+ "canonical_documents": list(self.canonical_documents),
87
+ "operations": [
88
+ {
89
+ "action": "write",
90
+ "path": write.path,
91
+ "content": None,
92
+ "reason": write.reason,
93
+ "diff": redact_secrets(write.diff[:PLAN_DIFF_LIMIT])[0],
94
+ "diff_truncated": len(write.diff) > PLAN_DIFF_LIMIT,
95
+ }
96
+ for write in self.writes
97
+ ] + [
98
+ {"action": "move", **asdict(move)} for move in self.moves
99
+ ] + ([{
100
+ "action": "write",
101
+ "path": AUDIT_BASELINE_PATH.as_posix(),
102
+ "content": None,
103
+ "reason": "record exact existing documentation debt after bootstrap",
104
+ "diff": None,
105
+ }] if self.accept_hygiene_baseline else []),
106
+ "gaps": list(self.gaps),
107
+ "model": self.model.as_dict() if self.model else None,
108
+ }
109
+
110
+
111
+ def _readme_path(root: Path) -> str:
112
+ names = [path.name for path in root.iterdir() if path.is_file()]
113
+ if "README.md" in names:
114
+ return "README.md"
115
+ candidates = sorted(
116
+ name for name in names if name.lower() == "readme.md"
117
+ )
118
+ return candidates[0] if candidates else "README.md"
119
+
120
+
121
+ def _manifest_text(
122
+ document: str = "README.md", canonical_documents: tuple[str, ...] = ()
123
+ ) -> str:
124
+ declared = tuple(path for path in canonical_documents if path != document)
125
+ include = ""
126
+ if declared:
127
+ include = " include:\n" + "".join(
128
+ f" - {json.dumps(path)}\n" for path in declared
129
+ )
130
+ return f"""\
131
+ version: 2
132
+ bindings:
133
+ - id: repository-surface
134
+ type: region
135
+ doc: {document}
136
+ region: repository-surface
137
+ extractor: repository-overview
138
+ source: {{path: .}}
139
+ renderer: markdown-fragment
140
+ projections:
141
+ llms_txt:
142
+ output: llms.txt
143
+ title: Repository documentation
144
+ summary: Bound facts and explicitly declared canonical repository context.
145
+ {include}
146
+ """
147
+
148
+
149
+ def _binding(document: str = "README.md") -> RegionBinding:
150
+ return RegionBinding(
151
+ id="repository-surface",
152
+ doc=Path(document),
153
+ region=REFERENCE_REGION,
154
+ extractor="repository-overview",
155
+ source=Source(Path(".")),
156
+ renderer="markdown-fragment",
157
+ columns=(),
158
+ )
159
+
160
+
161
+ def _reference_document(
162
+ root: Path, document: str, drafts: tuple[GroundedDraft, ...] = ()
163
+ ) -> str:
164
+ readme = root / document
165
+ existing = readme.exists()
166
+ title = "# Repository surface\n" if document == GENERATED_REFERENCE else "# Repository\n"
167
+ current = readme.read_text(encoding="utf-8") if existing else title
168
+ current, _redaction_rules = redact_secrets(current)
169
+ registered = REGISTER_PROFILE in current
170
+ if registered:
171
+ current = ensure_purpose_contract(current, fallback=False)
172
+ if not existing and document != GENERATED_REFERENCE:
173
+ lines = current.splitlines()
174
+ heading = next((index for index, line in enumerate(lines) if line.startswith("# ")), 0)
175
+ purpose = [
176
+ REGISTER_PROFILE,
177
+ "<!-- sourcebound:purpose -->",
178
+ "Use this repository guide when you need to run or change this project. Without a "
179
+ "source-bound overview, entry points and public surfaces can drift from the "
180
+ "implementation; after reading, you can locate the detected surfaces and verify "
181
+ "their current sources.",
182
+ "<!-- sourcebound:end purpose -->",
183
+ ]
184
+ lines[heading + 1:heading + 1] = ["", *purpose]
185
+ current = "\n".join(lines).rstrip() + "\n"
186
+ binding = _binding(document)
187
+ evidence = extract_repository_overview(RepositorySnapshot(root), binding)
188
+ generated = render(evidence, binding)
189
+ highlights = ""
190
+ if drafts:
191
+ highlights = "### Grounded highlights\n\n" + "\n".join(
192
+ f"- {draft.text}" for draft in drafts
193
+ ) + "\n\n"
194
+ section = (
195
+ "## Repository surface\n\n"
196
+ "This summary is a static catalog of detected package, CLI, API, schema, and test "
197
+ "surfaces. It does not validate existing prose claims. Direct manifest bindings are "
198
+ "the accuracy boundary; run `sourcebound inventory` for coverage state and the full "
199
+ "catalog.\n\n"
200
+ f"{highlights}"
201
+ f"<!-- sourcebound:begin {REFERENCE_REGION} -->\n"
202
+ f"{generated}\n"
203
+ f"<!-- sourcebound:end {REFERENCE_REGION} -->\n"
204
+ )
205
+ if f"<!-- sourcebound:begin {REFERENCE_REGION} -->" in current:
206
+ return replace_region(current, REFERENCE_REGION, generated)
207
+ if REFERENCE_SECTION.search(current):
208
+ return REFERENCE_SECTION.sub(section + "\n", current, count=1).rstrip() + "\n"
209
+ return current.rstrip() + "\n\n" + section
210
+
211
+
212
+ def _diff(path: str, before: str, after: str) -> str:
213
+ return "".join(difflib.unified_diff(
214
+ before.splitlines(keepends=True),
215
+ after.splitlines(keepends=True),
216
+ fromfile=path,
217
+ tofile=f"{path} (sourcebound init)",
218
+ ))
219
+
220
+
221
+ def _planned_write(root: Path, path: str, content: str, reason: str) -> PlannedWrite | None:
222
+ target = root / path
223
+ before = target.read_text(encoding="utf-8") if target.exists() else ""
224
+ if before == content:
225
+ return None
226
+ return PlannedWrite(path, content, reason, _diff(path, before, content))
227
+
228
+
229
+ def _archive_moves(root: Path) -> list[PlannedMove]:
230
+ # Filename and text-overlap heuristics cannot prove that a record is
231
+ # obsolete or that two files have the same owner. Init only plans
232
+ # reversible generated writes; archival remains an explicit operator act.
233
+ del root
234
+ return []
235
+
236
+
237
+ def _canonical_documents(
238
+ root: Path, readme_path: str, excluded: set[str]
239
+ ) -> tuple[str, ...]:
240
+ del root, excluded
241
+ return (readme_path,)
242
+
243
+
244
+ def build_bootstrap_plan(
245
+ root: Path,
246
+ provider: PhrasingProvider | None = None,
247
+ *,
248
+ accept_hygiene_baseline: bool = False,
249
+ ) -> BootstrapPlan:
250
+ root = root.resolve()
251
+ if not root.is_dir():
252
+ raise ConfigurationError(f"repository root does not exist or is not a directory: {root}")
253
+ readme_path = _readme_path(root)
254
+ readme = root / readme_path
255
+ binding_document = readme_path
256
+ if readme.exists() and REGISTER_PROFILE not in readme.read_text(encoding="utf-8"):
257
+ binding_document = GENERATED_REFERENCE
258
+ generated_reference = root / binding_document
259
+ if (
260
+ generated_reference.exists()
261
+ and f"<!-- sourcebound:begin {REFERENCE_REGION} -->"
262
+ not in generated_reference.read_text(encoding="utf-8")
263
+ ):
264
+ raise ConfigurationError(
265
+ f"init cannot replace the reserved generated document: {binding_document}"
266
+ )
267
+ archive_candidates = _archive_moves(root)
268
+ moves = [] if accept_hygiene_baseline else archive_candidates
269
+ canonical_documents = _canonical_documents(
270
+ root, readme_path, {item.source for item in archive_candidates}
271
+ )
272
+ existing_manifest = root / ".sourcebound.yml"
273
+ manifest_text = _manifest_text(binding_document, canonical_documents)
274
+ if existing_manifest.exists() and existing_manifest.read_text(encoding="utf-8") != manifest_text:
275
+ raise ConfigurationError(
276
+ "init cannot replace an existing manifest; remove it or run inventory and add bindings"
277
+ )
278
+ report = scan_inventory(root)
279
+ facts = tuple(item for item in report.items if item.kind in INCLUDED_KINDS)
280
+ model = build_model_record(root, facts, provider) if provider else None
281
+ reference = _reference_document(
282
+ root,
283
+ binding_document,
284
+ model.drafts if model else (),
285
+ )
286
+ moved = {item.source for item in moves}
287
+ manifest = Manifest(
288
+ path=root / ".sourcebound.yml",
289
+ version=1,
290
+ bindings=(_binding(binding_document),),
291
+ projections=ProjectionConfig(
292
+ llms_txt=LlmsTxtProjection(
293
+ Path("llms.txt"),
294
+ "Repository documentation",
295
+ "Bound facts and explicitly declared canonical repository context.",
296
+ tuple(Path(path) for path in canonical_documents if path != binding_document),
297
+ )
298
+ ),
299
+ )
300
+ assert manifest.projections is not None
301
+ llms_config = manifest.projections.llms_txt
302
+ assert llms_config is not None
303
+ indexed_documents = {
304
+ path: (
305
+ reference.encode("utf-8")
306
+ if path == binding_document
307
+ else (root / path).read_bytes()
308
+ )
309
+ for path in canonical_documents
310
+ }
311
+ indexed_documents[binding_document] = reference.encode("utf-8")
312
+ llms = render_llms_txt(
313
+ manifest,
314
+ title=llms_config.title,
315
+ summary=llms_config.summary,
316
+ documents=indexed_documents,
317
+ output_path=root / llms_config.output,
318
+ )
319
+ writes = [
320
+ item for item in (
321
+ _planned_write(
322
+ root,
323
+ binding_document,
324
+ reference,
325
+ "bind detected repository surfaces",
326
+ ),
327
+ _planned_write(root, ".sourcebound.yml", manifest_text, "declare the generated binding"),
328
+ _planned_write(root, "llms.txt", llms, "index the source-bound documentation"),
329
+ ) if item is not None
330
+ ]
331
+ purpose_gaps: list[str] = []
332
+ for path in list_documents(root):
333
+ relative = path.relative_to(root).as_posix()
334
+ if relative == binding_document or relative in moved:
335
+ continue
336
+ current = path.read_text(encoding="utf-8")
337
+ if REGISTER_PROFILE not in current:
338
+ continue
339
+ profile = classify_document(path.relative_to(root), current)
340
+ if not profile.applies("purpose-contract"):
341
+ continue
342
+ updated = ensure_purpose_contract(current, fallback=False)
343
+ if "<!-- sourcebound:purpose -->" not in updated:
344
+ purpose_gaps.append(
345
+ f"purpose contract needs authored judgment: {relative}"
346
+ )
347
+ continue
348
+ planned = _planned_write(
349
+ root,
350
+ relative,
351
+ updated,
352
+ "mark the document-level purpose contract",
353
+ )
354
+ if planned is not None:
355
+ writes.append(planned)
356
+ planned_content = {write.path: write.content for write in writes}
357
+ indexed_documents = {
358
+ path: (
359
+ planned_content[path]
360
+ if path in planned_content
361
+ else (root / path).read_text(encoding="utf-8")
362
+ ).encode("utf-8")
363
+ for path in canonical_documents
364
+ }
365
+ indexed_documents[binding_document] = (
366
+ planned_content.get(binding_document, reference).encode("utf-8")
367
+ )
368
+ final_llms = render_llms_txt(
369
+ manifest,
370
+ title=llms_config.title,
371
+ summary=llms_config.summary,
372
+ documents=indexed_documents,
373
+ output_path=root / llms_config.output,
374
+ )
375
+ writes = [write for write in writes if write.path != "llms.txt"]
376
+ llms_write = _planned_write(root, "llms.txt", final_llms, "index the canonical documentation")
377
+ if llms_write is not None:
378
+ writes.append(llms_write)
379
+ supported = {"Python", "TypeScript", "JavaScript"}
380
+ gaps = tuple(
381
+ f"language adapter missing: {language}"
382
+ for language in report.languages
383
+ if language not in supported
384
+ )
385
+ if not facts and not report.languages:
386
+ gaps += (
387
+ "no supported source facts detected; use a complete source checkout or add a binding manually",
388
+ )
389
+ if not accept_hygiene_baseline:
390
+ gaps += tuple(purpose_gaps)
391
+ introduced_secret = False
392
+ for item in writes:
393
+ target = root / item.path
394
+ before = target.read_text(encoding="utf-8") if target.exists() else ""
395
+ before_rules = set(redact_secrets(before)[1])
396
+ after_rules = set(redact_secrets(item.content)[1])
397
+ if after_rules - before_rules:
398
+ introduced_secret = True
399
+ break
400
+ if introduced_secret:
401
+ gaps += ("secret detected in generated documentation",)
402
+ payload = json.dumps({
403
+ "facts": [item.id + ":" + item.digest for item in facts],
404
+ "writes": [(item.path, hashlib.sha256(item.content.encode()).hexdigest()) for item in writes],
405
+ "moves": [(item.source, item.path) for item in moves],
406
+ "gaps": gaps,
407
+ "model": model.as_dict() if model else None,
408
+ "accept_hygiene_baseline": accept_hygiene_baseline,
409
+ "canonical_documents": canonical_documents,
410
+ }, sort_keys=True, separators=(",", ":"))
411
+ return BootstrapPlan(
412
+ facts,
413
+ tuple(writes),
414
+ tuple(moves),
415
+ gaps,
416
+ model,
417
+ hashlib.sha256(payload.encode()).hexdigest(),
418
+ canonical_documents,
419
+ accept_hygiene_baseline,
420
+ )
421
+
422
+
423
+ def apply_bootstrap_plan(root: Path, plan: BootstrapPlan) -> None:
424
+ root = root.resolve()
425
+ if plan.gaps:
426
+ raise ConfigurationError("cannot initialize unsupported surfaces: " + "; ".join(plan.gaps))
427
+ originals = {
428
+ write.path: (root / write.path).read_bytes() if (root / write.path).exists() else None
429
+ for write in plan.writes
430
+ }
431
+ parent_existed = {
432
+ (root / write.path).parent: (root / write.path).parent.exists()
433
+ for write in plan.writes
434
+ }
435
+ baseline_key = AUDIT_BASELINE_PATH.as_posix()
436
+ if plan.accept_hygiene_baseline and baseline_key not in originals:
437
+ baseline_path = root / AUDIT_BASELINE_PATH
438
+ originals[baseline_key] = (
439
+ baseline_path.read_bytes() if baseline_path.exists() else None
440
+ )
441
+ completed_moves: list[PlannedMove] = []
442
+ try:
443
+ for move in plan.moves:
444
+ source = root / move.source
445
+ destination = root / move.path
446
+ destination.parent.mkdir(parents=True, exist_ok=True)
447
+ if destination.exists():
448
+ raise ConfigurationError(f"archive destination already exists: {move.path}")
449
+ os.replace(source, destination)
450
+ completed_moves.append(move)
451
+ for write in plan.writes:
452
+ atomic_write(root / write.path, write.content)
453
+ results = evaluate(root, root / ".sourcebound.yml")
454
+ if any(result.changed for result in results):
455
+ raise ConfigurationError("init wrote a baseline that does not pass binding checks")
456
+ if plan.accept_hygiene_baseline:
457
+ write_audit_baseline(root)
458
+ report = audit(root)
459
+ if not report.ok:
460
+ details = "; ".join(
461
+ f"{finding.path}:{finding.line} {finding.rule}" for finding in report.findings[:5]
462
+ )
463
+ if report.stale_baseline:
464
+ details = (details + "; " if details else "") + "; ".join(
465
+ f"{finding.path}:{finding.line} stale-baseline"
466
+ for finding in report.stale_baseline[:5]
467
+ )
468
+ raise PolicyError(f"init baseline has policy findings: {details}")
469
+ manifest = load_manifest(root / ".sourcebound.yml")
470
+ if any(result.changed for result in evaluate_projections(root, manifest)):
471
+ raise ConfigurationError("init wrote a baseline with stale projections")
472
+ except Exception:
473
+ for path, content in originals.items():
474
+ target = root / path
475
+ if content is None:
476
+ target.unlink(missing_ok=True)
477
+ else:
478
+ atomic_write(target, content.decode("utf-8"))
479
+ if plan.accept_hygiene_baseline:
480
+ try:
481
+ (root / AUDIT_BASELINE_PATH).parent.rmdir()
482
+ except OSError:
483
+ pass
484
+ for parent, existed in sorted(
485
+ parent_existed.items(),
486
+ key=lambda item: len(item[0].parts),
487
+ reverse=True,
488
+ ):
489
+ if not existed:
490
+ try:
491
+ parent.rmdir()
492
+ except OSError:
493
+ pass
494
+ for move in reversed(completed_moves):
495
+ source = root / move.source
496
+ destination = root / move.path
497
+ if destination.exists():
498
+ source.parent.mkdir(parents=True, exist_ok=True)
499
+ os.replace(destination, source)
500
+ parent = destination.parent
501
+ while parent != root:
502
+ try:
503
+ parent.rmdir()
504
+ except OSError:
505
+ break
506
+ parent = parent.parent
507
+ raise