docs-drift 0.1.0__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 (131) hide show
  1. docs_drift/__init__.py +7 -0
  2. docs_drift/analysis.py +52 -0
  3. docs_drift/baseline.py +70 -0
  4. docs_drift/cli.py +871 -0
  5. docs_drift/collaboration.py +282 -0
  6. docs_drift/config.py +113 -0
  7. docs_drift/docs/__init__.py +1 -0
  8. docs_drift/docs/defaults.py +250 -0
  9. docs_drift/docs/discover.py +172 -0
  10. docs_drift/docs/extract_blocks.py +596 -0
  11. docs_drift/docs/extract_inline.py +561 -0
  12. docs_drift/docs/pipeline.py +234 -0
  13. docs_drift/docs/preprocess.py +71 -0
  14. docs_drift/docs/sections.py +247 -0
  15. docs_drift/errors.py +119 -0
  16. docs_drift/fix/__init__.py +1 -0
  17. docs_drift/fix/edits.py +124 -0
  18. docs_drift/fix/model.py +139 -0
  19. docs_drift/fix/rules.py +232 -0
  20. docs_drift/git_safety.py +35 -0
  21. docs_drift/graph/queries/evidence_path.cypher +6 -0
  22. docs_drift/history.py +412 -0
  23. docs_drift/history_cache.py +62 -0
  24. docs_drift/ids.py +50 -0
  25. docs_drift/index/__init__.py +211 -0
  26. docs_drift/index/base.py +239 -0
  27. docs_drift/index/config.py +185 -0
  28. docs_drift/index/js_cli.py +211 -0
  29. docs_drift/index/js_routes.py +68 -0
  30. docs_drift/index/js_symbols.py +219 -0
  31. docs_drift/index/python_cli.py +370 -0
  32. docs_drift/index/python_routes.py +102 -0
  33. docs_drift/index/python_source.py +20 -0
  34. docs_drift/index/python_symbols.py +335 -0
  35. docs_drift/index/queries/javascript.scm +4 -0
  36. docs_drift/index/queries/typescript.scm +4 -0
  37. docs_drift/index/routes_common.py +16 -0
  38. docs_drift/index/tasks.py +131 -0
  39. docs_drift/index/ts_parser.py +20 -0
  40. docs_drift/limits.py +18 -0
  41. docs_drift/llm/__init__.py +7 -0
  42. docs_drift/llm/candidates.py +81 -0
  43. docs_drift/llm/client.py +185 -0
  44. docs_drift/llm/pricing.py +54 -0
  45. docs_drift/llm/privacy.py +63 -0
  46. docs_drift/llm/prompts/rewrite_v1.md +9 -0
  47. docs_drift/llm/prompts/verify_v1.md +12 -0
  48. docs_drift/llm/prompts.py +28 -0
  49. docs_drift/llm/retrieve.py +60 -0
  50. docs_drift/llm/types.py +68 -0
  51. docs_drift/llm/verify.py +179 -0
  52. docs_drift/lsp.py +199 -0
  53. docs_drift/match/__init__.py +5 -0
  54. docs_drift/match/commands.py +201 -0
  55. docs_drift/match/defaults.py +79 -0
  56. docs_drift/match/engine.py +319 -0
  57. docs_drift/match/rename.py +69 -0
  58. docs_drift/models.py +249 -0
  59. docs_drift/ownership.py +70 -0
  60. docs_drift/policy.py +81 -0
  61. docs_drift/portfolio.py +147 -0
  62. docs_drift/py.typed +0 -0
  63. docs_drift/report/__init__.py +38 -0
  64. docs_drift/report/common.py +31 -0
  65. docs_drift/report/github.py +22 -0
  66. docs_drift/report/html.py +106 -0
  67. docs_drift/report/json.py +17 -0
  68. docs_drift/report/junit.py +30 -0
  69. docs_drift/report/markdown.py +58 -0
  70. docs_drift/report/partials/body.html +19 -0
  71. docs_drift/report/partials/report.css +12 -0
  72. docs_drift/report/partials/script.js +95 -0
  73. docs_drift/report/portfolio.html +14 -0
  74. docs_drift/report/report.html +8 -0
  75. docs_drift/report/sarif.py +68 -0
  76. docs_drift/report/terminal.py +90 -0
  77. docs_drift/report/vendor/CYTOSCAPE-LICENSE +19 -0
  78. docs_drift/report/vendor/cytoscape.min.js +32 -0
  79. docs_drift/report/workflow.html +12 -0
  80. docs_drift/review.py +113 -0
  81. docs_drift/safe_files.py +19 -0
  82. docs_drift/scanning.py +202 -0
  83. docs_drift/schemas/SARIF-LICENSE.md +7 -0
  84. docs_drift/schemas/SARIF-NOTICES.txt +3 -0
  85. docs_drift/schemas/sarif-schema-2.1.0.json +3389 -0
  86. docs_drift/schemas/scan-result.v1.json +1024 -0
  87. docs_drift/setup.py +130 -0
  88. docs_drift/snapshots.py +100 -0
  89. docs_drift/support.py +31 -0
  90. docs_drift/watch.py +157 -0
  91. docs_drift/web/__init__.py +1 -0
  92. docs_drift/web/__main__.py +23 -0
  93. docs_drift/web/app.py +289 -0
  94. docs_drift/web/cleanup.py +23 -0
  95. docs_drift/web/examples/clean-repo.json +646 -0
  96. docs_drift/web/examples/fastapi-svc.json +723 -0
  97. docs_drift/web/examples/files-to-prompt.json +3366 -0
  98. docs_drift/web/examples/judge-fixture.json +226 -0
  99. docs_drift/web/examples/manifest.json +89 -0
  100. docs_drift/web/examples/node-tool.json +823 -0
  101. docs_drift/web/examples/py-cli.json +1267 -0
  102. docs_drift/web/examples/rich-cli-reviewed.json +7855 -0
  103. docs_drift/web/examples/rich-cli.json +8010 -0
  104. docs_drift/web/examples/verification.json +16 -0
  105. docs_drift/web/git.py +274 -0
  106. docs_drift/web/jobs.py +352 -0
  107. docs_drift/web/settings.py +50 -0
  108. docs_drift/web/static/demo.css +37 -0
  109. docs_drift/web/static/demo.js +184 -0
  110. docs_drift/web/static/evidence/correction-verification.json +28 -0
  111. docs_drift/web/static/evidence/flow-highlights-2026-10-22.png +0 -0
  112. docs_drift/web/static/evidence/fresh-scan.json +7855 -0
  113. docs_drift/web/static/evidence/review.json +44 -0
  114. docs_drift/web/static/evidence/rich-cli-correction.patch +20 -0
  115. docs_drift/web/static/evidence/runtime.json +26 -0
  116. docs_drift/web/static/evidence/saved-scan.json +8010 -0
  117. docs_drift/web/static/evidence/usage-card-supplied.png +0 -0
  118. docs_drift/web/templates/base.html +34 -0
  119. docs_drift/web/templates/case.html +13 -0
  120. docs_drift/web/templates/landing.html +90 -0
  121. docs_drift/web/templates/scan.html +2 -0
  122. docs_drift/web/templates/voice.html +1 -0
  123. docs_drift/web/validation.py +47 -0
  124. docs_drift/web/worker.py +152 -0
  125. docs_drift/workflow_cli.py +96 -0
  126. docs_drift/workspaces.py +128 -0
  127. docs_drift-0.1.0.dist-info/METADATA +331 -0
  128. docs_drift-0.1.0.dist-info/RECORD +131 -0
  129. docs_drift-0.1.0.dist-info/WHEEL +4 -0
  130. docs_drift-0.1.0.dist-info/entry_points.txt +2 -0
  131. docs_drift-0.1.0.dist-info/licenses/LICENSE +21 -0
docs_drift/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Find drift between documentation and statically inspected code."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("docs-drift")
6
+
7
+ __all__ = ["__version__"]
docs_drift/analysis.py ADDED
@@ -0,0 +1,52 @@
1
+ """Aggregate evidence-backed findings and select findings touched by a Git change."""
2
+
3
+ import re
4
+ from collections import Counter
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from docs_drift.config import Config
9
+ from docs_drift.docs.pipeline import _head
10
+ from docs_drift.history import _run
11
+ from docs_drift.models import ScanResult
12
+ from docs_drift.scanning import run_scan
13
+
14
+
15
+ def insights(result: ScanResult) -> dict[str, Any]:
16
+ drift = [f for f in result.findings if f.is_drift and f.confidence >= result.config_used.min_confidence]
17
+ return {
18
+ "head_sha": result.head_sha,
19
+ "claims": len(result.claims),
20
+ "verdicts": result.summary()["by_verdict"],
21
+ "drift_by_document": dict(sorted(Counter(f.claim.doc_path for f in drift).items())),
22
+ "drift_by_package": dict(sorted(Counter(f.package or "." for f in drift).items())),
23
+ "with_introducing_commit": sum(f.introduced_by is not None for f in drift),
24
+ "warnings": result.warnings,
25
+ }
26
+
27
+
28
+ def changed_paths(repo: Path, base: str, head: str) -> list[str]:
29
+ for ref in (base, head):
30
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_./~^{}-]{0,199}", ref) or ".." in ref:
31
+ raise ValueError("Use a single Git revision without options, whitespace or revision ranges.")
32
+ result = _run(repo, ["diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", base, head, "--"])
33
+ if result is None:
34
+ raise ValueError("Cannot compare these revisions. Fetch the base commit or choose a revision that exists.")
35
+ return sorted(p.replace("\\", "/") for p in result.stdout.split("\0") if p)
36
+
37
+
38
+ def impact(repo: Path, config: Config, base: str, head: str = "HEAD") -> ScanResult:
39
+ """Report current findings linked to changed docs/source; not a runtime dependency proof."""
40
+ paths = set(changed_paths(repo, base, head))
41
+ resolved = _run(repo, ["rev-parse", "--verify", head])
42
+ if not resolved or resolved.stdout.strip() != _head(repo):
43
+ raise ValueError("Impact scans the current checkout; --head must resolve to its HEAD.")
44
+ result = run_scan(repo, config)
45
+ selected = [
46
+ f
47
+ for f in result.findings
48
+ if f.claim.doc_path in paths
49
+ or (f.matched_symbol and f.matched_symbol.file in paths)
50
+ or any(e.file in paths for e in f.evidence)
51
+ ]
52
+ return result.model_copy(update={"findings": selected, "claims": [f.claim for f in selected]})
docs_drift/baseline.py ADDED
@@ -0,0 +1,70 @@
1
+ """Read and write explicit finding baselines without running scanned code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
9
+
10
+ from docs_drift.models import DecidedBy, Finding, Verdict
11
+ from docs_drift.safe_files import metadata_text
12
+
13
+
14
+ class BaselineEntry(BaseModel):
15
+ model_config = ConfigDict(extra="forbid")
16
+
17
+ finding_id: str = Field(min_length=1)
18
+ verdict: Verdict
19
+ doc_path: str = Field(min_length=1)
20
+ claim: str = Field(min_length=1)
21
+ decided_by: DecidedBy = DecidedBy.rules
22
+ replayed: bool = False
23
+
24
+
25
+ class FindingBaseline(BaseModel):
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+ schema_version: Literal[1] = 1
29
+ findings: list[BaselineEntry] = Field(default_factory=list)
30
+
31
+
32
+ def finding_id(finding: Finding) -> str:
33
+ """Claim IDs are stable across line shifts and suitable for baseline updates."""
34
+ return finding.claim.claim_id
35
+
36
+
37
+ def load_baseline(path: Path) -> FindingBaseline:
38
+ try:
39
+ return FindingBaseline.model_validate_json(metadata_text(path))
40
+ except ValidationError:
41
+ # Pydantic's default diagnostics can echo claim text from the input file.
42
+ raise ValueError(
43
+ f"Invalid baseline JSON or schema at {path}. Regenerate it from a reviewed scan with baseline update."
44
+ ) from None
45
+ except OSError as exc:
46
+ raise ValueError(f"Cannot read baseline {path}: {exc}. Check the path and permissions.") from exc
47
+
48
+
49
+ def save_baseline(path: Path, findings: list[Finding]) -> None:
50
+ path.parent.mkdir(parents=True, exist_ok=True)
51
+ entries = [
52
+ BaselineEntry(
53
+ finding_id=finding_id(item),
54
+ verdict=item.verdict,
55
+ doc_path=item.claim.doc_path,
56
+ claim=item.claim.value,
57
+ decided_by=item.decided_by,
58
+ replayed=item.replayed,
59
+ )
60
+ for item in findings
61
+ if item.is_drift
62
+ ]
63
+ entries.sort(key=lambda item: (item.doc_path, item.claim, item.finding_id))
64
+ path.write_text(FindingBaseline(findings=entries).model_dump_json(indent=2) + "\n", encoding="utf-8")
65
+
66
+
67
+ def new_findings(findings: list[Finding], baseline: FindingBaseline) -> list[Finding]:
68
+ # A changed verdict must be reviewed again even when the claim stayed put.
69
+ known = {(entry.finding_id, entry.verdict) for entry in baseline.findings}
70
+ return [item for item in findings if item.is_drift and (finding_id(item), item.verdict) not in known]