项目文件夹

文件
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

245 行
8.7 KiB
Python
可执行文件

#!/usr/bin/env python3
"""Pre-commit hook: keep README.md claims verifiable against the repo.
README.md is the project storefront, and it drifts: files move, headings
get renamed, CLI entry points change, and example snippets keep naming
things that no longer exist. A 2026-07 audit found a benchmark command
that had never been runnable, a `reset` example missing its required
flag, and an MCP example querying a search engine that was never
implemented. This hook catches those drift classes at commit time, in
README.md, SECURITY.md, CONTRIBUTING.md, and every markdown file under
docs/:
1. Relative markdown links must point at files/directories that exist.
2. Anchor fragments (``docs/faq.md#some-heading`` or same-file
``#-benchmarks``) must match a real heading in the target file,
using GitHub's slug rules. ``#L10``/``#L10-L20`` line anchors must
be within the target file's line count.
3. ``python -m local_deep_research...`` examples must reference a real,
runnable module: a ``<module>.py`` file or a package directory
containing ``__main__.py`` under ``src/``.
4. ``engine="<name>"`` examples must name an engine registered in
``ENGINE_REGISTRY`` (parsed from engine_registry.py).
Links inside fenced code blocks are exempt from check 1/2 -- GitHub does
not render them, and they are usually example markup (e.g. runtime
``/static/...`` paths in an HTML snippet). Checks 3/4 deliberately DO
look inside code blocks; that is where the examples live.
External (http/https/mailto) links are not checked -- no network access
at commit time.
"""
import argparse
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
REPO_ROOT = Path(__file__).resolve().parent.parent
# Fixed files whose internal references this hook validates; every
# markdown file under docs/ is added at runtime in main().
CHECKED_FILES = ["README.md", "SECURITY.md", "CONTRIBUTING.md"]
ENGINE_REGISTRY_PATH = Path(
"src/local_deep_research/web_search_engines/engine_registry.py"
)
# Inline markdown links/images: [text](target) / ![alt](target "title").
# The target group stops at whitespace or ')' so optional "title" parts
# are excluded.
MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(\s*<?([^)<>\s]+)>?[^)]*\)")
# Relative href/src in inline HTML.
HTML_REF = re.compile(r"""(?:href|src)=["']([^"']+)["']""")
# `python -m some.module` (also matches python3).
PYTHON_M = re.compile(r"python3?\s+-m\s+([A-Za-z_][\w.]*)")
# engine="name" in example snippets.
ENGINE_KWARG = re.compile(r"""engine=["']([\w\-]+)["']""")
# GitHub file line anchors: #L10 or #L10-L20.
LINE_ANCHOR = re.compile(r"^L(\d+)(?:-L(\d+))?$")
# Registry entries: ` "name": EngineEntry(`.
REGISTRY_KEY = re.compile(r"^\s+\"([\w\-]+)\":\s*EngineEntry\(", re.M)
FENCE = re.compile(r"^\s*(```|~~~)")
def github_slug(heading: str) -> str:
"""Approximate GitHub's heading-to-anchor slug algorithm.
Lowercase, strip markdown link syntax, drop everything that is not a
letter/digit/space/hyphen/underscore (this removes emoji and other
punctuation), then turn spaces into hyphens.
"""
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", heading).strip().lower()
kept = [c for c in text if c.isalnum() or c in "-_ "]
return "".join(kept).replace(" ", "-")
def iter_headings(md_text: str):
"""Yield ATX heading texts, skipping fenced code blocks."""
in_fence = False
for line in md_text.splitlines():
if FENCE.match(line):
in_fence = not in_fence
continue
if in_fence:
continue
match = re.match(r"^#{1,6}\s+(.*)$", line)
if match:
yield match.group(1)
def heading_slugs(md_text: str) -> set:
return {github_slug(h) for h in iter_headings(md_text)}
def strip_fences(md_text: str) -> str:
"""Blank out fenced code blocks; GitHub renders no links inside them."""
out, in_fence = [], False
for line in md_text.splitlines():
if FENCE.match(line):
in_fence = not in_fence
out.append("")
continue
out.append("" if in_fence else line)
return "\n".join(out)
def check_anchor(target: Path, fragment: str) -> str:
"""Return an error message for a bad anchor, or '' if it resolves."""
line_match = LINE_ANCHOR.match(fragment)
if line_match:
last = int(line_match.group(2) or line_match.group(1))
lines = len(target.read_text(encoding="utf-8").splitlines())
if last > lines:
return (
f"line anchor #{fragment} exceeds file length ({lines} lines)"
)
return ""
if target.suffix.lower() not in (".md", ".markdown"):
# GitHub only generates heading anchors for rendered markdown.
return f"anchor #{fragment} on non-markdown file cannot be verified"
slugs = heading_slugs(target.read_text(encoding="utf-8"))
slug = fragment.lower()
if slug in slugs:
return ""
# GitHub dedupes duplicate headings by appending -1, -2, ...
base = re.sub(r"-\d+$", "", slug)
if base != slug and base in slugs:
return ""
return f"no heading matches anchor #{fragment}"
def check_links(md_path: Path, text: str) -> list:
errors = []
targets = MARKDOWN_LINK.findall(text) + HTML_REF.findall(text)
for raw_target in targets:
parsed = urlparse(raw_target)
if parsed.scheme or raw_target.startswith("//"):
continue # external; not checkable offline
rel_path, fragment = unquote(parsed.path), parsed.fragment
target = (md_path.parent / rel_path if rel_path else md_path).resolve()
if not target.exists():
errors.append(f"broken link: {raw_target} (file not found)")
continue
if fragment and target.is_file():
anchor_error = check_anchor(target, fragment)
if anchor_error:
errors.append(f"broken link: {raw_target} ({anchor_error})")
return errors
def check_python_modules(root: Path, text: str) -> list:
errors = []
for module in PYTHON_M.findall(text):
if not module.startswith("local_deep_research"):
continue
base = root / "src" / Path(*module.split("."))
if base.with_suffix(".py").is_file():
continue
if (base / "__main__.py").is_file():
continue
errors.append(
f"`python -m {module}` is not runnable: expected "
f"src/{'/'.join(module.split('.'))}.py or .../__main__.py"
)
return errors
def check_engine_names(root: Path, text: str) -> list:
engines = ENGINE_KWARG.findall(text)
if not engines:
return []
registry_file = root / ENGINE_REGISTRY_PATH
if not registry_file.is_file():
return [f"cannot verify engine names: {ENGINE_REGISTRY_PATH} missing"]
registered = set(
REGISTRY_KEY.findall(registry_file.read_text(encoding="utf-8"))
)
if not registered:
# The registry format changed; failing loudly beats silently
# skipping the check.
return [
f"could not parse any engine names from {ENGINE_REGISTRY_PATH}; "
"update REGISTRY_KEY in this hook"
]
return [
f'engine="{name}" is not a registered search engine '
f"(see {ENGINE_REGISTRY_PATH})"
for name in engines
if name not in registered
]
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--root",
type=Path,
default=REPO_ROOT,
help="Repository root to check against (test seam)",
)
args = parser.parse_args()
root = args.root.resolve()
files = [root / name for name in CHECKED_FILES] + sorted(
root.glob("docs/**/*.md")
)
all_errors = []
for md_path in files:
name = str(md_path.relative_to(root))
if not md_path.is_file():
all_errors.append((name, "file is missing"))
continue
text = md_path.read_text(encoding="utf-8")
for error in (
check_links(md_path, strip_fences(text))
+ check_python_modules(root, text)
+ check_engine_names(root, text)
):
all_errors.append((name, error))
if all_errors:
print("❌ DOCUMENTATION REFERENCES SOMETHING THAT DOES NOT EXIST")
print("=" * 60)
for name, error in all_errors:
print(f"📄 {name}: {error}")
print("=" * 60)
print("FIX: update the link/example to match the repo, or fix the")
print("hook if GitHub's anchor rules are approximated incorrectly")
print("(.pre-commit-hooks/check-readme-links.py). Links inside")
print("fenced code blocks are already exempt.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())