#!/usr/bin/env python3 """PATH AUDIT — no document may name a file or directory that does not exist. Why this exists --------------- The docs used to name directories, modules and tests that were never created. A reader who acts on a documented path loses trust in the whole document, and nobody can tell "we decided against this" from "this is wrong". So the rule is: **every path-shaped token written in backticks in a document must resolve**, including in the retired/deprecated registry (`docs/retired.md` is scanned too) — there, a fictional path reads the same as a real one. The invented names are deliberately not repeated here, so the examples cannot be copied back in; `git log -p docs/retired.md` shows what they were. What this check proves ---------------------- Every path-shaped token in inline code in the scanned documents is one of: (a) resolvable — it exists in this repo (see ROOT_DIRS, a token is resolved against several real roots because docs cite files as `config.rs`, not always `src/config.rs`); (b) attributed to an external repo by an owning prefix (EXEMPT_EXTERNAL), e.g. `stalwartlabs/jmap-client/src/client.rs`; (c) provably deleted — the file is gone but `git log --all --diff-filter=A` shows it was committed at some point (e.g. `docs/todo.md`); (d) on the commented CONVENTIONAL allowlist — a well-known filename that the prose explicitly says this repo does not have. Anything else is reported as an error. What it does NOT prove ---------------------- - That the path means what the prose says, same limitation as audit_anchors. - Anything outside inline code. Backticks are the contract: if you write a path in prose without backticks, this check cannot see it. - Bare filenames unambiguously: `client.rs` resolves against any file with that basename in the repo. - Anything in scripts. Code legitimately contains path-like string literals (this check is defined by string patterns, after all); those are not documentation claims. Non-path shapes that look like paths are excluded by is_noise(), and a `seg/` token only counts as a directory reference when it is the whole inline-code span. Both rules are structural rather than denylisted: is_noise recognises the shapes that cannot be repo-relative paths (absolute paths and URIs, dot-prefixed URL parts, dotted hosts, media types, JMAP method names, all-numeric pairs, shell and glob fragments), and the whole-span rule means `Email/changes`, `application/json` and `method/path/origin/失败类别` are never mistaken for a directory. Adding a special case to either should make you suspect the document instead. """ import os import re import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) DOCS = [ os.path.join(ROOT, "docs", f) for f in sorted(os.listdir(os.path.join(ROOT, "docs"))) if f.endswith(".md") ] AGENTS = os.path.join(ROOT, "AGENTS.md") TARGETS = [t for t in DOCS + [AGENTS] if os.path.isfile(t)] # Directories a doc legitimately roots a path at. Every entry is real. ROOT_DIRS = [ ".", "src", "src/domain", "src/domain/jmap", "docs", "scripts", "scripts/docs_check", "cloudflare-worker", "cloudflare-worker/src", "web", ] EXTS = "rs|js|mjs|ts|tsx|json|toml|ya?ml|md|sh|sql|cfg|lock|html|css|proto|txt" # A file token (ends in a known extension) or a directory token (ends in '/'). TOKEN = re.compile( r"(?