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.
- docs_drift/__init__.py +7 -0
- docs_drift/analysis.py +52 -0
- docs_drift/baseline.py +70 -0
- docs_drift/cli.py +871 -0
- docs_drift/collaboration.py +282 -0
- docs_drift/config.py +113 -0
- docs_drift/docs/__init__.py +1 -0
- docs_drift/docs/defaults.py +250 -0
- docs_drift/docs/discover.py +172 -0
- docs_drift/docs/extract_blocks.py +596 -0
- docs_drift/docs/extract_inline.py +561 -0
- docs_drift/docs/pipeline.py +234 -0
- docs_drift/docs/preprocess.py +71 -0
- docs_drift/docs/sections.py +247 -0
- docs_drift/errors.py +119 -0
- docs_drift/fix/__init__.py +1 -0
- docs_drift/fix/edits.py +124 -0
- docs_drift/fix/model.py +139 -0
- docs_drift/fix/rules.py +232 -0
- docs_drift/git_safety.py +35 -0
- docs_drift/graph/queries/evidence_path.cypher +6 -0
- docs_drift/history.py +412 -0
- docs_drift/history_cache.py +62 -0
- docs_drift/ids.py +50 -0
- docs_drift/index/__init__.py +211 -0
- docs_drift/index/base.py +239 -0
- docs_drift/index/config.py +185 -0
- docs_drift/index/js_cli.py +211 -0
- docs_drift/index/js_routes.py +68 -0
- docs_drift/index/js_symbols.py +219 -0
- docs_drift/index/python_cli.py +370 -0
- docs_drift/index/python_routes.py +102 -0
- docs_drift/index/python_source.py +20 -0
- docs_drift/index/python_symbols.py +335 -0
- docs_drift/index/queries/javascript.scm +4 -0
- docs_drift/index/queries/typescript.scm +4 -0
- docs_drift/index/routes_common.py +16 -0
- docs_drift/index/tasks.py +131 -0
- docs_drift/index/ts_parser.py +20 -0
- docs_drift/limits.py +18 -0
- docs_drift/llm/__init__.py +7 -0
- docs_drift/llm/candidates.py +81 -0
- docs_drift/llm/client.py +185 -0
- docs_drift/llm/pricing.py +54 -0
- docs_drift/llm/privacy.py +63 -0
- docs_drift/llm/prompts/rewrite_v1.md +9 -0
- docs_drift/llm/prompts/verify_v1.md +12 -0
- docs_drift/llm/prompts.py +28 -0
- docs_drift/llm/retrieve.py +60 -0
- docs_drift/llm/types.py +68 -0
- docs_drift/llm/verify.py +179 -0
- docs_drift/lsp.py +199 -0
- docs_drift/match/__init__.py +5 -0
- docs_drift/match/commands.py +201 -0
- docs_drift/match/defaults.py +79 -0
- docs_drift/match/engine.py +319 -0
- docs_drift/match/rename.py +69 -0
- docs_drift/models.py +249 -0
- docs_drift/ownership.py +70 -0
- docs_drift/policy.py +81 -0
- docs_drift/portfolio.py +147 -0
- docs_drift/py.typed +0 -0
- docs_drift/report/__init__.py +38 -0
- docs_drift/report/common.py +31 -0
- docs_drift/report/github.py +22 -0
- docs_drift/report/html.py +106 -0
- docs_drift/report/json.py +17 -0
- docs_drift/report/junit.py +30 -0
- docs_drift/report/markdown.py +58 -0
- docs_drift/report/partials/body.html +19 -0
- docs_drift/report/partials/report.css +12 -0
- docs_drift/report/partials/script.js +95 -0
- docs_drift/report/portfolio.html +14 -0
- docs_drift/report/report.html +8 -0
- docs_drift/report/sarif.py +68 -0
- docs_drift/report/terminal.py +90 -0
- docs_drift/report/vendor/CYTOSCAPE-LICENSE +19 -0
- docs_drift/report/vendor/cytoscape.min.js +32 -0
- docs_drift/report/workflow.html +12 -0
- docs_drift/review.py +113 -0
- docs_drift/safe_files.py +19 -0
- docs_drift/scanning.py +202 -0
- docs_drift/schemas/SARIF-LICENSE.md +7 -0
- docs_drift/schemas/SARIF-NOTICES.txt +3 -0
- docs_drift/schemas/sarif-schema-2.1.0.json +3389 -0
- docs_drift/schemas/scan-result.v1.json +1024 -0
- docs_drift/setup.py +130 -0
- docs_drift/snapshots.py +100 -0
- docs_drift/support.py +31 -0
- docs_drift/watch.py +157 -0
- docs_drift/web/__init__.py +1 -0
- docs_drift/web/__main__.py +23 -0
- docs_drift/web/app.py +289 -0
- docs_drift/web/cleanup.py +23 -0
- docs_drift/web/examples/clean-repo.json +646 -0
- docs_drift/web/examples/fastapi-svc.json +723 -0
- docs_drift/web/examples/files-to-prompt.json +3366 -0
- docs_drift/web/examples/judge-fixture.json +226 -0
- docs_drift/web/examples/manifest.json +89 -0
- docs_drift/web/examples/node-tool.json +823 -0
- docs_drift/web/examples/py-cli.json +1267 -0
- docs_drift/web/examples/rich-cli-reviewed.json +7855 -0
- docs_drift/web/examples/rich-cli.json +8010 -0
- docs_drift/web/examples/verification.json +16 -0
- docs_drift/web/git.py +274 -0
- docs_drift/web/jobs.py +352 -0
- docs_drift/web/settings.py +50 -0
- docs_drift/web/static/demo.css +37 -0
- docs_drift/web/static/demo.js +184 -0
- docs_drift/web/static/evidence/correction-verification.json +28 -0
- docs_drift/web/static/evidence/flow-highlights-2026-10-22.png +0 -0
- docs_drift/web/static/evidence/fresh-scan.json +7855 -0
- docs_drift/web/static/evidence/review.json +44 -0
- docs_drift/web/static/evidence/rich-cli-correction.patch +20 -0
- docs_drift/web/static/evidence/runtime.json +26 -0
- docs_drift/web/static/evidence/saved-scan.json +8010 -0
- docs_drift/web/static/evidence/usage-card-supplied.png +0 -0
- docs_drift/web/templates/base.html +34 -0
- docs_drift/web/templates/case.html +13 -0
- docs_drift/web/templates/landing.html +90 -0
- docs_drift/web/templates/scan.html +2 -0
- docs_drift/web/templates/voice.html +1 -0
- docs_drift/web/validation.py +47 -0
- docs_drift/web/worker.py +152 -0
- docs_drift/workflow_cli.py +96 -0
- docs_drift/workspaces.py +128 -0
- docs_drift-0.1.0.dist-info/METADATA +331 -0
- docs_drift-0.1.0.dist-info/RECORD +131 -0
- docs_drift-0.1.0.dist-info/WHEEL +4 -0
- docs_drift-0.1.0.dist-info/entry_points.txt +2 -0
- docs_drift-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Discover bounded documentation files without importing or executing repository code."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Iterator, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
|
|
11
|
+
from pathspec import GitIgnoreSpec
|
|
12
|
+
|
|
13
|
+
from docs_drift.config import Config
|
|
14
|
+
from docs_drift.models import DocFile, DocFormat
|
|
15
|
+
|
|
16
|
+
MAX_DOC_BYTES = 2_000_000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DiscoveryWarning(UserWarning):
|
|
20
|
+
"""A documentation candidate or discovery mechanism could not be used."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _warn(message: str) -> None:
|
|
24
|
+
warnings.warn(message, DiscoveryWarning, stacklevel=3)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _globs(patterns: Sequence[str]) -> GitIgnoreSpec:
|
|
28
|
+
# Anchor configured globs to the scan root; **/ may match zero directories.
|
|
29
|
+
return GitIgnoreSpec.from_lines("/" + pattern.replace("\\", "/").lstrip("/") for pattern in patterns)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tracked_paths(repo: Path) -> list[str] | None:
|
|
33
|
+
# A directory inside some unrelated parent checkout is not itself a repo root.
|
|
34
|
+
if not (repo / ".git").exists():
|
|
35
|
+
return None
|
|
36
|
+
env = {key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_")}
|
|
37
|
+
env["GIT_OPTIONAL_LOCKS"] = "0"
|
|
38
|
+
# Repository-configured fsmonitor commands/hooks must never run during a scan.
|
|
39
|
+
command = [
|
|
40
|
+
"git",
|
|
41
|
+
"--no-pager",
|
|
42
|
+
"-c",
|
|
43
|
+
"core.fsmonitor=false",
|
|
44
|
+
"-c",
|
|
45
|
+
f"core.hooksPath={os.devnull}",
|
|
46
|
+
"-C",
|
|
47
|
+
str(repo),
|
|
48
|
+
"ls-files",
|
|
49
|
+
"-z",
|
|
50
|
+
"--cached",
|
|
51
|
+
"--exclude-standard",
|
|
52
|
+
]
|
|
53
|
+
try:
|
|
54
|
+
tracked = subprocess.run(command, check=True, capture_output=True, env=env, timeout=5).stdout
|
|
55
|
+
ignored = subprocess.run([*command, "--ignored"], check=True, capture_output=True, env=env, timeout=5).stdout
|
|
56
|
+
except (OSError, subprocess.SubprocessError):
|
|
57
|
+
_warn("Cannot list tracked documentation with Git; walking the repository and its .gitignore files instead.")
|
|
58
|
+
return None
|
|
59
|
+
ignored_paths = set(ignored.split(b"\0"))
|
|
60
|
+
return [os.fsdecode(path) for path in tracked.split(b"\0") if path and path not in ignored_paths]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class _IgnoreLayer:
|
|
65
|
+
directory: PurePosixPath
|
|
66
|
+
spec: GitIgnoreSpec
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ignored(path: str, is_directory: bool, layers: tuple[_IgnoreLayer, ...]) -> bool:
|
|
70
|
+
ignored = False
|
|
71
|
+
for layer in layers:
|
|
72
|
+
relative = PurePosixPath(path).relative_to(layer.directory).as_posix()
|
|
73
|
+
result = layer.spec.check_file(relative + ("/" if is_directory else ""))
|
|
74
|
+
if result.include is not None:
|
|
75
|
+
ignored = result.include
|
|
76
|
+
return ignored
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _inside(repo: Path, path: Path) -> bool:
|
|
80
|
+
return not path.is_symlink() and path.resolve().is_relative_to(repo)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _walk(repo: Path, directory: Path, layers: tuple[_IgnoreLayer, ...] = ()) -> Iterator[str]:
|
|
84
|
+
ignore_file = directory / ".gitignore"
|
|
85
|
+
try:
|
|
86
|
+
if ignore_file.is_file() and _inside(repo, ignore_file):
|
|
87
|
+
spec = GitIgnoreSpec.from_lines(ignore_file.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
88
|
+
layers = (*layers, _IgnoreLayer(PurePosixPath(directory.relative_to(repo).as_posix()), spec))
|
|
89
|
+
children = sorted(directory.iterdir())
|
|
90
|
+
except OSError:
|
|
91
|
+
_warn(f"Cannot read directory {directory}; check its permissions.")
|
|
92
|
+
return
|
|
93
|
+
vendors = {".git", ".hg", ".svn", ".venv", "venv", "node_modules", "vendor", "__pycache__", ".docs-drift"}
|
|
94
|
+
for child in children:
|
|
95
|
+
relative = child.relative_to(repo).as_posix()
|
|
96
|
+
if not _inside(repo, child):
|
|
97
|
+
continue
|
|
98
|
+
is_directory = child.is_dir()
|
|
99
|
+
if _ignored(relative, is_directory, layers):
|
|
100
|
+
continue
|
|
101
|
+
if is_directory:
|
|
102
|
+
if child.name not in vendors:
|
|
103
|
+
yield from _walk(repo, child, layers)
|
|
104
|
+
elif child.is_file():
|
|
105
|
+
yield relative
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _format(path: str) -> DocFormat | None:
|
|
109
|
+
return {
|
|
110
|
+
".md": DocFormat.markdown,
|
|
111
|
+
".markdown": DocFormat.markdown,
|
|
112
|
+
".mdx": DocFormat.mdx,
|
|
113
|
+
".rst": DocFormat.rst,
|
|
114
|
+
".txt": DocFormat.text,
|
|
115
|
+
".text": DocFormat.text,
|
|
116
|
+
}.get(PurePosixPath(path).suffix.casefold())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_bytes(repo: Path, relative: str) -> bytes:
|
|
120
|
+
path = repo / relative
|
|
121
|
+
if not _inside(repo, path) or not path.is_file():
|
|
122
|
+
raise ValueError(f"{relative} is not a regular file inside the repository. Rediscover documentation.")
|
|
123
|
+
if path.stat().st_size > MAX_DOC_BYTES:
|
|
124
|
+
raise ValueError(f"{relative} exceeds 2 MB and was skipped. Narrow the configured documentation globs.")
|
|
125
|
+
with path.open("rb") as stream:
|
|
126
|
+
content = stream.read(MAX_DOC_BYTES + 1)
|
|
127
|
+
if len(content) > MAX_DOC_BYTES:
|
|
128
|
+
raise ValueError(f"{relative} exceeds 2 MB and was skipped. Narrow the configured documentation globs.")
|
|
129
|
+
return content
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def discover_docs(repo: Path, config: Config) -> list[DocFile]:
|
|
133
|
+
"""Select tracked files in Git roots, otherwise walk with layered .gitignore rules."""
|
|
134
|
+
root = repo.resolve()
|
|
135
|
+
if not root.is_dir():
|
|
136
|
+
raise ValueError("Choose an existing repository directory before discovering documentation.")
|
|
137
|
+
patterns = config.docs
|
|
138
|
+
if patterns == Config().docs:
|
|
139
|
+
from docs_drift.workspaces import docs_patterns # noqa: PLC0415 - optional dependency or shared pipeline import.
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
patterns = [*patterns, *docs_patterns(root)]
|
|
143
|
+
except OSError as exc:
|
|
144
|
+
# Documentation-only discovery can still use its bounded walk.
|
|
145
|
+
# Full scans will reject the unavailable source index separately.
|
|
146
|
+
_warn(f"Cannot inspect documentation-site markers: {exc}")
|
|
147
|
+
includes, excludes = _globs(patterns), _globs(config.exclude)
|
|
148
|
+
paths = _tracked_paths(root)
|
|
149
|
+
candidates = _walk(root, root) if paths is None else iter(paths)
|
|
150
|
+
docs = []
|
|
151
|
+
for relative in sorted(set(candidates)):
|
|
152
|
+
format_ = _format(relative)
|
|
153
|
+
if format_ is None or not includes.match_file(relative) or excludes.match_file(relative):
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
content = _read_bytes(root, relative)
|
|
157
|
+
except (OSError, ValueError) as exc:
|
|
158
|
+
_warn(f"Cannot read documentation {relative}: {exc}")
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
docs.append(DocFile(path=relative, content_hash=hashlib.sha256(content).hexdigest(), format=format_))
|
|
162
|
+
except ValueError:
|
|
163
|
+
_warn(f"Cannot read documentation {relative!r}: unsupported path; skipped.")
|
|
164
|
+
return docs
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def read_doc(repo: Path, doc: DocFile) -> str:
|
|
168
|
+
"""Read the discovered byte snapshot as UTF-8 with replacement, detecting intervening edits."""
|
|
169
|
+
content = _read_bytes(repo.resolve(), doc.path)
|
|
170
|
+
if hashlib.sha256(content).hexdigest() != doc.content_hash:
|
|
171
|
+
raise ValueError(f"{doc.path} changed since discovery. Rediscover documentation before parsing it.")
|
|
172
|
+
return content.decode("utf-8", errors="replace")
|