555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
148 lines
4.7 KiB
Python
148 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Check docs/llms.txt coverage against docs/**/*.mdx.
|
|
|
|
Two-way diff:
|
|
- pages present in docs/ but not linked in docs/llms.txt -> "missing"
|
|
- URLs linked in docs/llms.txt that point to no page -> "stale"
|
|
|
|
Modes:
|
|
default read-only; exits 1 if any drift is found.
|
|
--write appends placeholder entries for missing pages under a
|
|
"## Unclassified - needs triage" H2 at the end of
|
|
docs/llms.txt. Stale URLs are reported but never removed
|
|
automatically (human decides whether a page was renamed
|
|
or genuinely deleted). Exits 1 if anything was written,
|
|
0 if the file was already in sync.
|
|
|
|
Exit codes:
|
|
0 in sync, or --write mode completed (workflow continues to open a PR)
|
|
1 read-only drift detected
|
|
|
|
The script has no third-party dependencies; stdlib only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
DOCS_DIR = REPO_ROOT / "docs"
|
|
LLMS_TXT = DOCS_DIR / "llms.txt"
|
|
IGNORE_FILE = REPO_ROOT / "scripts" / "llms-txt-ignore.txt"
|
|
|
|
BASE_URL = "https://docs.mem0.ai/"
|
|
TRIAGE_HEADER = "## Unclassified - needs triage"
|
|
URL_RE = re.compile(r"\(https://docs\.mem0\.ai/([^)\s#]*)")
|
|
|
|
|
|
def load_ignore_prefixes() -> list[str]:
|
|
if not IGNORE_FILE.exists():
|
|
return []
|
|
prefixes: list[str] = []
|
|
for raw in IGNORE_FILE.read_text().splitlines():
|
|
line = raw.strip()
|
|
if line and not line.startswith("#"):
|
|
prefixes.append(line)
|
|
return prefixes
|
|
|
|
|
|
def canonical_repo_pages(ignore_prefixes: list[str]) -> tuple[set[str], set[str]]:
|
|
all_pages: set[str] = set()
|
|
included_pages: set[str] = set()
|
|
for path in DOCS_DIR.rglob("*.mdx"):
|
|
rel = path.relative_to(DOCS_DIR).with_suffix("").as_posix()
|
|
all_pages.add(rel)
|
|
if not any(rel.startswith(p) for p in ignore_prefixes):
|
|
included_pages.add(rel)
|
|
return all_pages, included_pages
|
|
|
|
|
|
def indexed_urls(text: str) -> set[str]:
|
|
urls: set[str] = set()
|
|
for match in URL_RE.finditer(text):
|
|
path = match.group(1).rstrip("/")
|
|
if path:
|
|
urls.add(path)
|
|
return urls
|
|
|
|
|
|
def format_placeholder(page: str) -> str:
|
|
title = page.rsplit("/", 1)[-1].replace("-", " ").replace("_", " ").title()
|
|
return (
|
|
f"- [{title}]({BASE_URL}{page}) [TODO: Platform|OSS|Both]: "
|
|
"TODO - rewrite as 'Use when ...' and move into the correct section."
|
|
)
|
|
|
|
|
|
def append_triage_block(text: str, missing_pages: list[str]) -> str:
|
|
entries = "\n".join(format_placeholder(p) for p in missing_pages)
|
|
if TRIAGE_HEADER in text:
|
|
return text.rstrip() + "\n" + entries + "\n"
|
|
preamble = (
|
|
"> New docs pages were detected by CI. For each entry below: replace "
|
|
"the scope tag with `[Platform]`, `[OSS]`, or `[Both]`; rewrite the "
|
|
"description as `Use when ...`; move it into the correct section; "
|
|
"then delete this H2 once empty.\n"
|
|
)
|
|
return text.rstrip() + "\n\n" + TRIAGE_HEADER + "\n\n" + preamble + "\n" + entries + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--write",
|
|
action="store_true",
|
|
help="scaffold placeholder entries in docs/llms.txt for missing pages",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if not LLMS_TXT.exists():
|
|
print(f"ERROR: {LLMS_TXT} not found.", file=sys.stderr)
|
|
return 1
|
|
|
|
ignore_prefixes = load_ignore_prefixes()
|
|
all_pages, included_pages = canonical_repo_pages(ignore_prefixes)
|
|
text = LLMS_TXT.read_text()
|
|
linked = indexed_urls(text)
|
|
|
|
missing = sorted(included_pages - linked)
|
|
stale = sorted(linked - all_pages)
|
|
|
|
if missing:
|
|
print(f"Pages missing from docs/llms.txt ({len(missing)}):")
|
|
for p in missing:
|
|
print(f" + {p}")
|
|
if stale:
|
|
print(f"\nURLs in docs/llms.txt with no matching .mdx page ({len(stale)}):")
|
|
for p in stale:
|
|
print(f" - {p}")
|
|
print(
|
|
"\n(Stale URLs are never auto-removed: check whether the page was "
|
|
"renamed, and update the link by hand.)"
|
|
)
|
|
|
|
if not missing and not stale:
|
|
print("docs/llms.txt is in sync with docs/**/*.mdx.")
|
|
return 0
|
|
|
|
if args.write:
|
|
if missing:
|
|
new_text = append_triage_block(text, missing)
|
|
LLMS_TXT.write_text(new_text)
|
|
print(
|
|
f"\nAppended {len(missing)} placeholder entries under "
|
|
f"'{TRIAGE_HEADER}' in docs/llms.txt."
|
|
)
|
|
else:
|
|
print("\nNo additions to scaffold; stale URLs surfaced above require manual cleanup.")
|
|
return 0
|
|
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|