# Staleproof — full documentation > Staleproof keeps a software organisation’s documentation true to its code. For a GitHub pull request it walks a FalkorDB graph of code (Python, TypeScript and JavaScript symbols and call edges across repositories), docs (Markdown, MDX and reStructuredText sections and code examples), maintainers’ decisions and past verdicts; flags the examples whose calls no longer fit the changed code; runs the impacted docs examples before and after the change in a sandbox; and proposes the docs fix with the graph path behind each paragraph. With no pull request it audits the docs (a health score, examples that no longer fit the code, API coverage) and checks a working tree before it is pushed. Open source (MIT), published on npm as `staleproof`. Index: https://staleproof.dev/llms.txt --- # Staleproof documentation Staleproof keeps a software organisation's documentation true to its code. This page explains what it does, the problem it solves, how a check works, and where to find everything else in these docs. Start here if you are new; jump to [Install](install.md) or the [Quickstart](getting-started.md) if you want to run it. ## What it is `staleproof` is an open-source (MIT) command-line tool, GitHub App, GitHub Action, editor extension, Claude Code plugin, pre-commit hook, MCP server, HTTP API and Node.js library, also hosted at [staleproof.dev](https://staleproof.dev). It indexes your repositories into a [FalkorDB](https://www.falkordb.com) graph: Python, TypeScript, JavaScript and Go symbols and the calls between them across repositories, every Markdown, MDX and reStructuredText doc section and code example, and the code each one mentions or calls, with every example call checked against the signature it reaches. For a GitHub pull request it walks that graph to find every doc section the change can make false, predicts which examples the change breaks before anything runs, runs the affected Python, JavaScript and TypeScript examples before and after the change in a sandbox, and (in agent mode) proposes the docs fix with the path in the graph behind each paragraph. Everything it concludes is written back to the graph, so the next check starts from what the last one learned. With no pull request, it checks a working tree before it is pushed, lists the docs that depend on a function before you change it, and audits the docs as they are, with a 0-100 health score. Without any graph, [`staleproof lint`](lint.md) checks the paths, links, anchors, commands, CLI flags, environment variables and symbols that the docs, READMEs and the files AI coding agents read (`AGENTS.md`, `CLAUDE.md`, Cursor and Copilot rules) name, against the repository itself, and [`staleproof fix`](fix.md) applies the fixes it can verify. The same findings appear in pull requests (annotations, suggestions), in your editor as you type, in Claude Code after each edit, and on `git commit`. ## The problem Docs drift for two reasons that code review does not catch: - **The paragraph is in another repository.** A client library, the SDK built on it and the docs site usually live in separate repositories. A change to a function in the client can falsify a page in the docs site that never names that function: the page calls an SDK method, which calls a client method, which calls the function that changed. Nobody reviewing the client pull request sees that page. - **Nobody re-runs the examples.** Code examples in docs are written once. When the library under them changes, or when they were never quite right, they stay broken until a reader reports it. Searching for the changed names does not solve either problem, because the docs that break are usually the ones that reach the changed code indirectly. ## A worked example [falkordb-py#262](https://github.com/FalkorDB/falkordb-py/pull/262) changed one function, `Is_Cluster`. The graph path is: ```text Is_Cluster ← FalkorDB.__init__ ← README "Asynchronous Example" ``` The README example never names `Is_Cluster`; it calls the constructor, which calls it. Staleproof ran that example (`FalkorDB/falkordb-py:README.md:4`) against falkordb-py installed from source at both commits. It **fails before the pull request** with `TypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry'` and **passes after it**: the pull request fixed a broken README example. A whole-word grep for `Is_Cluster` finds no doc section at all. The same holds at larger scale. Across the four pull requests checked on FalkorDB's repositories, the graph reaches 120 doc sections; a whole-word grep for the changed names finds 30 of them and matches 227 other sections that no path from the change reaches. For the three falkordb-py pull requests, grep finds none of the 47 sections the graph reaches. On [falkordb-py#276](https://github.com/FalkorDB/falkordb-py/pull/276), 4 changed symbols reach 43 doc sections, 41 of them in other repositories. Every number here comes from [Facts from the graph](FACTS.md), which is generated from the graph. ## How a check works 1. **Index.** `staleproof index` (or the first check of a repository) clones your GitHub repositories, or downloads them where there is no git, and writes one graph per organisation: Python, TypeScript, JavaScript and Go symbols and calls (resolved across repositories through imports, types and re-exports), doc sections and code examples, the symbols each one mentions or uses, and the example lines that no longer fit the code they call. 2. **Ingest the pull request.** The diff between the pull request's two commits becomes symbol-level changes (removed, renamed, signature, modified, added, export-removed), attached to the pull request in the graph. 3. **Trace.** Staleproof walks from each changed symbol back through up to 3 callers, in any repository, to the docs that mention or call them. That is the impact. The changed symbols with no such path are the clearance. For each impacted example it compares the call as written with the code after the pull request: a removed name or a call the new signature rejects is a predicted break. 4. **Prove.** For impacted Python, JavaScript and TypeScript examples, Staleproof installs the repositories from source at the commit before the pull request and at the pull request, runs the example in a sandbox against a scratch FalkorDB, and records both runs. 5. **Judge and remember.** In agent mode a model reads each impacted section, checks maintainers' decisions and earlier verdicts, and records a verdict or proposes an edit for every one. The docs patch is built from those edits. Every check, run and verdict stays in the graph. The terms used above, in plain language: - **Impact**: the doc sections and code examples a pull request can make false. A section is impacted when the graph has a path from a changed symbol, through up to 3 callers, to the section (because it mentions one of those symbols, or one of its examples calls one). - **Evidence path**: that path, written out node by node, changed symbol first and doc last, as in the #262 example above. It is the reason a section is listed, and Staleproof refuses to edit a section without one. - **Predicted break**: an example whose call fits the code before the pull request and not after it (a removed or renamed name, a new required argument, a keyword that no longer exists), found statically, before anything runs. - **Mismatch**: an example line that does not fit the code it calls right now, found when the docs are indexed. The [docs audit](audit.md) lists them all, with a health score. - **Clearance**: the changed symbols that no doc reaches within 3 call hops. The graph proves the absence; each one comes with a `git grep` command so you can check the claim yourself. - **Verdict**: the conclusion about one section for one pull request: `stale` (with replacement text), `ok`, `governed` (a maintainer decision says it stays as it is) or `skipped`. Maintainers can accept or reject each verdict. - **Verdict memory**: verdicts, example runs, maintainer decisions and checks stay in the graph. Later checks read them: an identical example run is answered from the graph, a decision is cited instead of re-flagging the section, and earlier verdicts (with whether maintainers accepted them) inform the next judgement. - **Agent mode and graph mode**: in agent mode a model chooses which tools to call and in what order, judges every impacted section and proposes edits. It needs model API credentials (Claude, or an Anthropic-compatible provider). Graph mode runs the same tools in a fixed order with no model and samples rather than judges: impact, clearance, a ranking of the first impact page and at most 3 example runs. It writes no verdicts and no edits, and needs no key. ## Ways to use it | Interface | What it is for | Docs | |---|---|---| | GitHub App | [Install it](https://github.com/apps/staleproof) on a repository, public or private, no workflow file: a check run on every pull request with the docs it makes wrong, a comment when there is something to say, suggestions for the verified fixes and an **Apply fixes** button | [GitHub App](github-app.md) | | Website | Paste a pull request URL of any public repository on [staleproof.dev](https://staleproof.dev): indexed on the fly, checked in graph mode, nothing installed; or [audit a repository](https://staleproof.dev/#/audit) and get its README badge | [Install](install.md), [Docs audit](audit.md) | | CLI | `npx -y staleproof `: index, check a pull request or your working tree, audit, list the docs for a symbol, run an example, record a decision, build the patch; the first command starts FalkorDB and indexes the repository itself | [CLI](cli.md) | | GitHub Action | `uses: rajkaria/staleproof@v1`, no inputs or secrets: a comment on every pull request with the docs it can make false, the evidence path for each, the examples predicted to break and the examples run before and after, and the verified fixes as suggestions; on a schedule, a docs audit kept in one issue or a weekly fix pull request; on a release, the release gate. On the [GitHub Marketplace](https://github.com/marketplace/actions/staleproof-docs-check) | [GitHub Action](github-action.md), [Fix stale docs](fix.md), [Release gate](release.md) | | Editors | The VS Code and Cursor extension `staleproof.staleproof` (Visual Studio Marketplace, Open VSX) and `staleproof lsp` for Neovim, Zed, Helix and JetBrains: lint findings as you type, verified quick fixes | [Editors (LSP)](editors.md) | | Claude Code plugin | `/plugin marketplace add rajkaria/staleproof`, then `/plugin install staleproof@staleproof`: Claude hears which docs each edit made wrong, and fixes them before it stops | [Claude Code plugin](claude-code.md) | | pre-commit | A hook that stops a commit whose staged changes make a docs reference wrong (pre-commit, lefthook, husky) | [Pre-commit hooks](pre-commit.md) | | Docs site | A "checked against `abc1234`" stamp on each page: Docusaurus and MkDocs plugins, any HTML | [Checked stamp](verified-stamp.md) | | MCP server | The tools for Claude Code, Claude Desktop, claude.ai, Cursor, VS Code or any MCP client: hosted at `https://staleproof.dev/api/mcp`, or local over stdio | [MCP server](mcp.md) | | HTTP API | `staleproof serve`: checks and audits streamed over server-sent events, the README badge, traces, patches, decisions, and the web UI | [HTTP API](http-api.md) | | Node.js library | Index, check, patch and serve from your own TypeScript code | [Node.js library](library.md) | | Self-host | Vercel with FalkorDB Cloud, Docker Compose on a VM, or a single machine without Docker | [Deploy](DEPLOY.md) | ## Status - **Built and run** against FalkorDB's real repositories on the author's machine: the indexer, cross-repository impact, clearance, PageRank ranking, sandboxed example runs, decisions and verdict memory, the CLI, the MCP server, the web UI and graph-mode checks. - **Agent mode** was written for Claude (default model `claude-sonnet-5-5`) and runs on any Anthropic-compatible API through `ANTHROPIC_BASE_URL`. It has been run with a real model through DeepSeek's Anthropic-compatible endpoint (`deepseek-v4-pro`, also what the live demo uses). The Claude path is tested against a local fake API only. - **No edit proposed on FalkorDB's repositories yet.** The pull requests checked there are fixes, and every verdict recorded on #262, #267 and #276 is `ok`. Proposing edits and building the patch from them are exercised by the test suite on a fixture organisation. - **Run end to end** on a private test repository: the GitHub Action's pull request check from the one-click workflow, its suggestions (posted once, a committed one clearing its finding), its weekly fix pull request (opened, updated, closed), and the GitHub App's check runs, comments and suggestions, also next to the Action. - **Written, not yet exercised**: the Docker image has not been built. - **Live**: [staleproof.dev](https://staleproof.dev), in hosted mode: any public repository is indexed on first use into a workspace graph of its own, and MCP answers at `/api/mcp`. Examples are not executed there (no sandbox on Vercel); for FalkorDB's repositories, the before/after results come from runs recorded on the graph. Also live: the npm package [`staleproof`](https://www.npmjs.com/package/staleproof), the [GitHub App](https://github.com/apps/staleproof), the Action on the [GitHub Marketplace](https://github.com/marketplace/actions/staleproof-docs-check), the editor extension on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=staleproof.staleproof) and [Open VSX](https://open-vsx.org/extension/staleproof/staleproof), and the Claude Code plugin. ## Limits - Code is read as Python, TypeScript and JavaScript in published npm packages, or Go; docs as Markdown, MDX or reStructuredText. Python, JavaScript and TypeScript examples are run; Go examples are not. Keyword checks are Python only; JavaScript, TypeScript and Go calls get positional argument counts. Other languages are indexed as text. - Static checks and predicted breaks judge only calls resolved through imports and types; where the source cannot settle a call, they stay silent. - GitHub repositories only. Private ones need `GITHUB_TOKEN` (the CLI, a server you run, the GitHub Action) or the GitHub App, which reads the repositories it is installed on into a graph per installation; the hosted site and hosted MCP server index public repositories only. - Call resolution is static. What types do not reveal falls back to by-name edges (labelled as such) or to no edge. - The example sandbox is a guard, not a security boundary, and example runs use today's third-party dependencies. ## Documentation map **Start here** - [Introduction](README.md): this page. - [Quickstart](getting-started.md): three ways to start (website, GitHub Action, one command), then a local FalkorDB, FalkorDB's three repositories indexed, a pull request checked, an example run, the patch, the web UI and MCP. - [Install](install.md): the website, the GitHub Action, the hosted and local MCP servers and the CLI, with what each needs and does. - [Use cases](use-cases.md): SDK teams with a separate docs site, DevRel after a release, maintainers reviewing pull requests, a weekly docs audit, developers before they push, AI coding agents (with and without a pull request), platform teams with many repositories, and recording maintainer decisions. - [Docs audit](audit.md): how true the docs are with no pull request: the checks, the score, the CLI, the web page and badge, the API, MCP and the scheduled Action. **Integrate** - [CLI](cli.md): every command and option, with examples, output and exit codes. - [GitHub Action](github-action.md): one-click install, what a run does, inputs and outputs, permissions, the comment format, the docs fix pull request, the scheduled audit and keeping a graph between runs. - [MCP server](mcp.md): the hosted endpoint, client configuration, the fourteen tools, result format, worked sessions (with and before a pull request) and security notes. - [HTTP API](http-api.md): every endpoint of `staleproof serve`, including indexing on demand, audits, the badge and MCP over HTTP. - [Node.js library](library.md): using Staleproof from TypeScript. **How it works** - [Core concepts](concepts.md): impact, evidence paths, severity, clearance, static checks, predicted breaks and the audit, verdicts, decisions, runs, checks, the two modes, ids. - [Indexing](indexing.md): which files are read (Python, TypeScript, JavaScript; Markdown, MDX, reStructuredText), how calls are resolved across repositories, how docs link to and are checked against code, example directives, re-index semantics. - [Example runner](example-runner.md): virtualenvs per commit, the sandbox, the scratch FalkorDB, skip rules, run outcomes. - [Decisions and memory](memory.md): maintainer decisions, supersession, verdict review, what every check writes back. - [The agent](agent.md): the loop, its system prompt and stop condition, model providers, budgets, graph mode. **Reference** - [Tools reference](tools.md): the fourteen tools, their parameters and the Cypher each runs. - [Configuration](configuration.md): every environment variable and global option, and the per-repository `.staleproof.toml`. - [Graph model and Cypher](GRAPH-MODEL.md): every Cypher statement Staleproof sends to FalkorDB, generated from the code. - [Facts from the graph](FACTS.md): every number quoted about FalkorDB's repositories, generated from the graph. - [Architecture](ARCHITECTURE.md): source layout and a check end to end. **Operate** - [Deploy](DEPLOY.md): Vercel with FalkorDB Cloud, Docker Compose, or no Docker; hosted mode and its limits. - [Troubleshooting and FAQ](troubleshooting.md): common errors, limits and answers. ## For LLMs and tools The site at [staleproof.dev](https://staleproof.dev) serves these docs as plain Markdown: an index at [/llms.txt](https://staleproof.dev/llms.txt), every page in one file at [/llms-full.txt](https://staleproof.dev/llms-full.txt), and each page at `/docs/.md` (this page is [/docs/introduction.md](https://staleproof.dev/docs/introduction.md)). The repository carries the same index as [llms.txt](../llms.txt), linking to the Markdown files on GitHub. ## Related - [Quickstart](getting-started.md) - [Use cases](use-cases.md) - [Core concepts](concepts.md) - [Facts from the graph](FACTS.md) --- # Quickstart From nothing to a checked pull request. First the four ways to start: the GitHub App with no workflow file, no install on the website, one click for the GitHub Action, or one command. Then the full walkthrough: start FalkorDB, index FalkorDB's three public repositories, check a real pull request with and without a model, run one docs example before and after it, build the patch, check your own changes before there is a pull request, open the web UI and connect Claude Code; then your own repositories. Every command here is copy-pasteable; you need no account other than an optional model API key. ## Four ways to start ### No workflow file: the GitHub App Click **Install the GitHub App** on [staleproof.dev/#/install](https://staleproof.dev/#/install), pick the repositories (public or private) and click **Install**. Every pull request then gets a **Staleproof** check run with the docs, READMEs and agent files it makes wrong, a comment when there is something to say, each verified fix inside the diff as a one-click suggestion, and an **Apply fixes** button for the rest. It runs no examples; the Action below does. See [GitHub App](github-app.md). ### No install: the website Open [staleproof.dev](https://staleproof.dev) and paste the URL of a pull request in any public Python, TypeScript or JavaScript repository on GitHub. The first check of a repository downloads and indexes it on the fly, then checks the pull request in graph mode: the doc sections it can make false with the evidence path for each, the changed symbols no doc reaches, and the ranking. Examples are not run on the hosted service, which has no sandbox. A docs site that lives in another repository can be named next to it, so both are indexed together. ### One click: the GitHub Action On [staleproof.dev/#/install](https://staleproof.dev/#/install), type your repository and click **Add to owner/repo**: GitHub opens its "new file" page with `.github/workflows/staleproof.yml` filled in, and you click **Commit changes**. Or copy [examples/staleproof.yml](../examples/staleproof.yml) into the repository yourself. From then on every pull request gets one comment with the docs it can make false and the examples run before and after it. It needs no secrets and no database: the job starts its own FalkorDB. See [GitHub Action](github-action.md). ### One command: the CLI ```bash npx -y staleproof check https://github.com///pull/ --no-agent ``` With no FalkorDB configured, the CLI starts a local one (falkordblite, no Docker), indexes the pull request's repository at its default branch head on first use, checks the pull request in graph mode and runs up to 3 affected examples before and after it. Add `--with /` when the docs live in another repository, and set `GITHUB_TOKEN` for a private repository. Without `--no-agent` the agent runs when model credentials are set ([step 4](#4-check-it-with-the-agent)); without them `check` prints a note and runs graph mode. It needs Node.js, git and uv or `python3` (below). [Install](install.md) compares these with the hosted and local MCP servers. The rest of this page walks through each part step by step. ## Prerequisites | You need | For | Notes | |---|---|---| | Node.js 20 or newer | everything | The package declares `node >= 20`. | | `git` | example runs; indexing, pull request ingestion and `patch` when it is installed | Repositories are cloned over HTTPS, blob-less, into `~/.staleproof/repos//`, sending `GITHUB_TOKEN` when it is set. Without git, indexing downloads a tarball of each repository and pull requests are read through the GitHub API, but examples cannot run. | | `bash` | the local FalkorDB (`db start`, or the one any command starts when none is configured) and the scratch FalkorDB examples run against | Both use the bundled `scripts/falkordb-local.sh`. | | [uv](https://docs.astral.sh/uv/) (recommended), or `python3` 3.12 or newer with `pip` | the local FalkorDB, the scratch FalkorDB, example runs | The local server comes from the `falkordblite` wheel (redis-server plus the FalkorDB module), which needs Python 3.12 or newer; with uv, a Python 3.12 is fetched for you. Example runs build one virtualenv per set of commits: with uv they use Python 3.12; with only `python3` they use whatever version `python3` is. | | `tar` | example runs | Sources are extracted with `git archive \| tar -x`. | | Docker (optional) | running FalkorDB in a container instead of the local server | | | `ANTHROPIC_API_KEY` (optional) | agent mode | Or an Anthropic-compatible provider; see [step 4](#4-check-it-with-the-agent). | | `GITHUB_TOKEN` (optional) | private repositories, pull request metadata, `patch --open-pr` | Without a token, GitHub allows 60 API requests an hour and only public repositories can be read. `GH_TOKEN` is read too. | You can run every command with `npx -y staleproof `, or install it once with `npm install -g staleproof` and drop the `npx -y`. See [CLI](cli.md#install-or-run-with-npx). ## 1. Start FalkorDB Staleproof keeps everything in a FalkorDB graph. The CLI connects to `FALKORDB_URL`, which defaults to `falkor://localhost:6379`. Pick one of the three options below. You can also skip this step: when neither `FALKORDB_URL` nor `--db` is set and nothing answers on `localhost:6379`, any command starts the local server described next and leaves it running. ### Without Docker ```bash npx -y staleproof db start ``` The first start installs `falkordblite` into `~/.staleproof/falkordb/venv`, then runs a FalkorDB server on `127.0.0.1:6379`. Data persists as RDB snapshots in `~/.staleproof/falkordb/data-6379`, so the graph, including verdicts and decisions, survives restarts. Stop it with `npx -y staleproof db stop`; check it with `npx -y staleproof db status`. ### With Docker ```bash docker run -d --name falkordb -p 6379:6379 falkordb/falkordb:latest ``` Examples run against a second, disposable FalkorDB that is flushed before every run. By default Staleproof starts that scratch server itself on port 6390 with the same script as `db start`, so it still needs uv or `python3`. To keep FalkorDB entirely in Docker, start the scratch server as a container too and point Staleproof at it: ```bash docker run -d --name staleproof-scratch -p 6390:6379 falkordb/falkordb:latest export STALEPROOF_SCRATCH_URL=falkor://localhost:6390 ``` Never point `STALEPROOF_SCRATCH_URL` at a server that holds data you need. Staleproof refuses to flush the server named in `FALKORDB_URL`, but any other server is wiped before each example run. The scratch server cannot require a password: docs examples connect to it without one, so a `STALEPROOF_SCRATCH_URL` with credentials is refused. ### FalkorDB Cloud Create an instance, then set its endpoint and password (user `falkordb`). URL-encode the password, and use `falkors://` if the endpoint has TLS: ```bash export FALKORDB_URL='falkor://falkordb:@:' ``` Leave the scratch server local (the default) or point `STALEPROOF_SCRATCH_URL` at a different disposable server. [Deploy](DEPLOY.md) covers FalkorDB Cloud in more detail. ## 2. Index FalkorDB's three repositories ```bash npx -y staleproof index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs ``` This clones the three repositories (the client library, the SDK built on it, and the docs site), parses their Python code and Markdown/MDX docs, resolves calls across all three, checks every docs example against the code it calls, and writes the graph `falkordb`: a command that names a repository uses the graph of its owner, lowercased, unless `--org` or `STALEPROOF_ORG` names another. Progress goes to stderr; at the end it prints one line per repository with its commit, symbol and call counts, one line per docs repository with sections, examples and links, and the dependencies it found between the repositories (for example `FalkorDB/GraphRAG-SDK → FalkorDB/falkordb-py (declares falkordb)`). Index related repositories in one command: calls and doc links resolve across repositories only within a single `index` run. Re-running the same command later is safe; it updates the graph in place and marks what disappeared as gone instead of deleting it. ## 3. Check a pull request in graph mode Graph mode needs no model. It runs the tools in a fixed order and samples rather than judges: impact, clearance, a PageRank ranking of the first impact page (up to 40 sections), then at most 3 example runs before and after the pull request. It writes no verdicts. ```bash npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/262 --no-agent ``` The first check of a pull request reads its metadata from GitHub and diffs its two commits into symbol-level changes. The first example run also builds virtualenvs and starts the scratch FalkorDB, so it takes longer than later ones. ### What the output shows From top to bottom: 1. **The pull request**: its title, URL, state, and the two commits compared (`before..after`). For a merged pull request, `before` is the first parent of the merge commit and `after` is the merge commit. 2. **Changed symbols**: each changed function, method or class with the kind of change (`removed`, `renamed`, `signature`, `modified`, `added`, `export-removed`). #262 changed one function, `Is_Cluster`. 3. **Impacted doc sections**: each section with a severity label (`BREAKING` for removed or renamed symbols and removed exports, `SIGNATURE` for signature changes, `BEHAVIOR` for body changes), its repository, the evidence path (`path: Is_Cluster ← FalkorDB.__init__ ← …`), the number of hops, the confidence (`exact` when every edge was resolved through imports and types, `by-name` or `late` otherwise), how many examples it has, and a link to the section on GitHub at the indexed commit. Sections are ordered by the PageRank of their page in the docs' own link graph, most central first. Only the first 40 sections are listed; a line such as `… 3 more` says how many were left out. For #262 there is one: the README's "Asynchronous Example". 4. **Cleared**: changed symbols that reach no doc within 3 hops, each with its raw text-match count in the docs and the `git grep` command to check it. `Is_Cluster` reaches a doc, so nothing is cleared for #262. 5. **Example runs**: for at most 3 examples on first-page sections whose path is `exact` (a sample, not every example), a `PASS`, `FAIL`, `TIMEOUT`, `SKIPPED` or `ERROR` line for each side with the commits installed, and the traceback of a failure. On the graph behind [Facts from the graph](FACTS.md), the README's Asynchronous Example failed before #262 with `TypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry'` and passed after it: the pull request fixed a broken example. Your run installs today's third-party dependencies, so it can differ. 6. **The summary and the check id**: one paragraph, then `Stored as chk-…`. The check, its full trace and every run are stored in the graph. A whole-word grep for `Is_Cluster` finds no doc section: the README example calls the constructor, which calls the changed function. That is the case the graph exists for. ## 4. Check it with the agent In agent mode a model chooses which tools to call, reads each impacted section, runs examples, checks maintainer decisions, and records a verdict or proposes an edit for every impacted section, on every impact page. ### Claude ```bash export ANTHROPIC_API_KEY='' npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/262 ``` The default model is `claude-sonnet-5-5` with effort `medium`; change them with `--model` and `--effort` (or `STALEPROOF_MODEL` and `STALEPROOF_EFFORT`). An `ant auth login` profile works instead of the key. The Claude path is tested against a local fake API only. ### DeepSeek or another Anthropic-compatible provider ```bash export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic export ANTHROPIC_API_KEY='' export STALEPROOF_MODEL=deepseek-v4-pro npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/262 ``` Any `ANTHROPIC_BASE_URL` whose host is not `api.anthropic.com` switches to compatible mode, which sends only the portable request fields. `STALEPROOF_MODEL` is required there: a `claude-*` model name is refused, because the provider would answer with its own model under that name. This is the path the live demo and the published agent runs use. The output is the trace: one line per tool call (`→ impact(pr_url=…)`), one per result (`← ` with the Cypher queries it ran and their time), the model's summarised reasoning, and a final line with the status (`Done`, `Incomplete`, `refused` or `error`), turns, tool calls, verdicts and tokens, followed by the model's summary. The last line names the patch command and the stored check id. The check is `Done` only when every impacted section (all 43 for #276, not only the first page of 40) has a verdict or an edit; if the model stops early it is reminded of the open sections up to twice, then the check is recorded as `Incomplete` with those sections listed. The tool order differs from one pull request to the next; for #262 the published run called `impact → read_section → decisions_for → run_example → record_verdict`. A large pull request takes longer: a 43-section check takes about 6 minutes. Without credentials, agent mode stops with an error that names the variables to set. Graph mode always works. ## 5. Run one example before and after Every code example has an id, `owner/repo:path:index`, where `index` is the block's position among all fenced blocks in the file (0-based). Impact output and `--json` list them. ```bash npx -y staleproof run "FalkorDB/falkordb-py:README.md:4" --pr https://github.com/FalkorDB/falkordb-py/pull/262 ``` It prints the example id and URL, one line per side (`FAIL before …`, `PASS after …`) with the commits installed, any traceback, and a one-line verdict such as `Fails at and passes at : the PR fixes this example.` If the graph already holds an identical run (same program, same commits), it is answered from the graph and marked as recorded. To execute it again and replace the stored run: ```bash npx -y staleproof run "FalkorDB/falkordb-py:README.md:4" --pr https://github.com/FalkorDB/falkordb-py/pull/262 --fresh ``` ## 6. Get the patch ```bash npx -y staleproof patch https://github.com/FalkorDB/falkordb-py/pull/262 ``` The patch is built from the edits the agent proposed (`stale` verdicts not rejected by a maintainer). It prints a unified diff that `git apply` accepts in each docs repository, then a pull request title and description with the evidence path, the example runs and the cited decisions for every edited section, and a list of the sections checked and left unchanged. Graph mode writes no verdicts, so after a graph-mode check alone the diff is empty and the command prints `(no proposed edits)`. The same is true when the agent concludes that nothing needs editing, which is what has happened on FalkorDB's repositories so far: the pull requests checked there are fixes, and every verdict recorded on #262, #267 and #276 is `ok`. A re-run on #267 considered fixing the GRAPH.EXPLAIN example, which fails before and after the pull request, and declined, because the page assumes a demo graph that already exists; it recommended a separate fix. Proposed edits and the patch built from them are exercised by the test suite on a fixture organisation, not yet on these repositories. The file is read at the commit the graph indexed: from the local clone in `~/.staleproof/repos` when there is one, otherwise from `raw.githubusercontent.com`. A section whose text changed since indexing, or whose file is gone, is listed as not applied instead of being patched. After an agent check that proposed edits, write the diff and the description to files, or open the fix as a pull request in your fork (needs `GITHUB_TOKEN`): ```bash npx -y staleproof patch --out docs-fix.diff npx -y staleproof patch --open-pr ``` ## 7. No pull request yet? Two commands work before anything is pushed, and one with no change at all. In a checkout of a repository the graph holds (or will index on first use): ```bash npx -y staleproof docs-for Graph.query --repo FalkorDB/falkordb-py # which docs depend on it, before you change it npx -y staleproof check # which docs your uncommitted and unpushed changes make false npx -y staleproof audit FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs # how true the docs are right now ``` `check` with no URL lists the impacted sections and the examples predicted to break, each with its line and the reason, without running anything; add `--fail-on-predicted` to use it as a pre-push hook ([CLI](cli.md#check-without-a-pull-request)). `audit` scores the docs from 0 to 100 and lists every example line that no longer fits the code ([Docs audit](audit.md)). ## 8. Open the web UI ```bash npx -y staleproof serve ``` Open `http://127.0.0.1:8080`. Paste a pull request URL, pick agent mode (when the server has model credentials) or graph mode, and watch the trace stream in. The startup line on stderr says which graph is served and whether agent mode is on. Use `--port` and `--host` to change the address. The same process serves the [HTTP API](http-api.md) and MCP over HTTP at `/api/mcp`. With `--hosted`, it indexes any public repository on demand into a graph of its own, as staleproof.dev does. ## 9. Add it to Claude Code ```bash claude mcp add staleproof -- npx -y staleproof mcp ``` Claude Code now has the fourteen tools, on the graph `falkordb` you indexed (the stdio server uses `STALEPROOF_ORG` when set, else the graph of the repository's owner, like the CLI). Ask it, for example: "What docs can https://github.com/FalkorDB/falkordb-py/pull/267 make false? Show the evidence path for each." It answers with `check_pull_request`, or step by step with `impact`, `read_section` and `run_example`; before a pull request, it can call `docs_for_symbol` and `check_changes` on what it is editing. To skip the local setup, add the hosted server instead: `claude mcp add --transport http staleproof https://staleproof.dev/api/mcp` (no example runs there). [MCP server](mcp.md) covers other clients and a full session. ## Index your own repositories ### What works - **Code repositories in Python.** Every `.py` file is parsed, except tests (`test/`, `tests/`, `testing/`, `test_*.py`, `*_test.py`, `conftest.py`), `setup.py`, and files under `scripts/`, `benchmarks/` or `docs/`. Python files under `example/`, `examples/`, `sample/`, `samples/`, `tutorial/`, `tutorials/`, `notebook/` or `notebooks/` become runnable example scripts. - **TypeScript and JavaScript libraries.** The source of every published npm package in the repository (a `package.json` with a `name`, not `private`; monorepos too), except tests and build tooling. JavaScript and TypeScript examples in the docs, and `examples/*.js` or `*.ts` scripts, are linked to it, checked for names that do not exist and for argument counts, and run in Node against packages built from source. - **Go modules.** Every package under a `go.mod` (multi-module repositories and `go.work` too). Go examples in the docs (` ```go `) are checked for missing or unexported names, argument counts and packages; they are not run. - **Docs in Markdown, MDX or reStructuredText.** Every `.md`, `.mdx` and `.rst` file in any indexed repository, except housekeeping files (`CHANGELOG`, `CONTRIBUTING`, `LICENSE` and similar). Sphinx docs work: code blocks, doctests, `:func:`/`:meth:`/`:class:` roles and `:doc:`/`:ref:` links. A docs site can be its own repository with no code at all. Notebooks are not read. - **Examples you do not want run** can be marked in the docs (``) or in a `.staleproof.toml` ([Configuration](configuration.md#repository-configuration)). - **GitHub repositories, public or private.** The CLI, a server you run and the GitHub Action read private repositories with `GITHUB_TOKEN` (or `GH_TOKEN`) set to a token that can read them; the Action's default token reads its own repository. The hosted site, staleproof.dev, indexes public repositories only. [Indexing](indexing.md) has the full rules. ### Index everything in one command Each organisation gets its own graph, named after the owner of the first repository (`acme` here) unless `--org` or `STALEPROOF_ORG` names it. Pass all its repositories to one `index` command: ```bash npx -y staleproof index acme/acme-client acme/acme-sdk acme/acme-docs ``` Calls and doc links resolve across repositories only among the repositories of one `index` run. If you index a repository on its own, or re-index it alone later, its calls into the other repositories and its docs' links into their code are re-resolved against that repository only, and those cross-repository edges disappear. Re-index with the same full command. To index a specific branch, tag or commit of one repository, use `--ref` (repeatable). Use `origin/`, a tag or a SHA rather than a bare branch name: ```bash npx -y staleproof --org acme index acme/acme-client acme/acme-docs --ref acme/acme-docs=origin/next ``` ### Check your pull requests `check` uses the graph of the pull request's owner, so a pull request in `acme/acme-client` reads `acme`. If that graph does not have the repository yet, `check` indexes it first, together with the repositories you pass to `--with`; `--no-index` makes it stop with ` is not indexed in the "" graph` instead. ```bash npx -y staleproof check https://github.com/acme/acme-client/pull/123 --no-agent ``` When one graph holds repositories of several owners (a docs site under another account, say), pass the same `--org` to every command, or set `STALEPROOF_ORG`, so they all use that graph. ## Next steps - Compare every way to install it: [Install](install.md). - Comment on every pull request automatically: [GitHub Action](github-action.md). - Learn every command and option: [CLI](cli.md). - Find the scenario closest to yours: [Use cases](use-cases.md). - Record decisions so intentional docs stop being re-flagged: [Decisions and memory](memory.md). - Run it for a team: [Deploy](DEPLOY.md). ## Related - [CLI](cli.md) - [Use cases](use-cases.md) - [MCP server](mcp.md) - [Configuration](configuration.md) - [Troubleshooting and FAQ](troubleshooting.md) --- # Install Every way to use Staleproof, in the order the site's install page, [staleproof.dev/#/install](https://staleproof.dev/#/install), offers them: the GitHub App first (one click, no workflow file), the GitHub Action second (the way to run docs examples today), then the website, your editor, Claude Code, an MCP server for your AI assistant, the CLI, a pre-commit hook and `staleproof init`, which sets several of them up at once. Each section says what it needs, what it does, and what it does not do. | Way | Needs | Runs examples | Repositories | |---|---|---|---| | [GitHub App](#github-app-no-workflow-file) | Installing the App on the repository | No | Public and private | | [GitHub Action](#github-action-one-click) | A workflow file in the repository | Yes, before and after each pull request | Public and private | | [Website](#website-no-install) | A browser | No | Public | | [Editors](#editors-while-you-type) | VS Code, Cursor or another LSP-capable editor; Node.js 20+, git | No | Any local checkout | | [Claude Code plugin](#claude-code-plugin) | Claude Code; Node.js 20+, git | No | Any local checkout | | [MCP, hosted](#mcp-hosted-no-install) | An MCP client | No | Public | | [MCP, local](#mcp-local) | Node.js 20+, git, uv or `python3` | Yes | Public and private (with `GITHUB_TOKEN`) | | [CLI](#cli-one-command) | Node.js 20+, git, uv or `python3` | Yes | Public and private (with `GITHUB_TOKEN`) | | [pre-commit](#pre-commit-on-every-commit) | pre-commit, lefthook or husky; Node.js 20+ | No | Any local checkout | | [`staleproof init`](#staleproof-init-set-it-all-up) | Node.js 20+, git | Sets up the ones above | Any local checkout | ## GitHub App: no workflow file Click **Install the GitHub App** on [staleproof.dev/#/install](https://staleproof.dev/#/install) (it opens [github.com/apps/staleproof](https://github.com/apps/staleproof)), pick the repositories, and click **Install**: no YAML, no secrets. The next pull request is checked. - **What it does.** Every pull request gets a **Staleproof** check run with the docs references it makes wrong (as annotations), the doc sections its code changes reach and the examples predicted to break, a comment only when there is something to say, each verified fix inside the diff as a one-click suggestion, and an **Apply fixes** button on the check run that commits the rest to the pull request's branch. Private repositories are read with the installation's token and indexed into a graph no public endpoint can read. Once a day it can keep a fix pull request and an audit issue up to date (`[app] fix-prs`, `audit-issue`). - **What it does not do.** It runs no examples and has no agent mode: the [GitHub Action](github-action.md) does both, in your own CI. The two work side by side; a suggestion one of them already posted on the same lines is not posted again. Permissions, private code and the `[app]` settings: [GitHub App](github-app.md). ## GitHub Action: one click The Action is listed on the GitHub Marketplace as [Staleproof docs check](https://github.com/marketplace/actions/staleproof-docs-check). On [staleproof.dev/#/install](https://staleproof.dev/#/install), type the repository and click **Add to owner/repo**. GitHub opens its "new file" page with `.github/workflows/staleproof.yml` filled in; click **Commit changes**. Options on the page add a docs repository to index with it (`also-index`), agent mode (needs an `ANTHROPIC_API_KEY` repository secret), failing the check when an example breaks (`fail-on: breaks`), and a weekly audit of the whole docs kept in one issue. Or copy [examples/staleproof.yml](../examples/staleproof.yml) yourself: ```yaml name: Staleproof on: pull_request: permissions: contents: read pull-requests: write jobs: docs: runs-on: ubuntu-latest steps: - uses: rajkaria/staleproof@v1 ``` - **What it does.** On every pull request the job starts a FalkorDB, indexes the repository, checks the change (the docs references it makes wrong, as annotations, and the examples predicted to break), runs up to 3 affected examples at the commit before the pull request and at the pull request, posts the verified fixes inside the diff as suggestions (`suggestions`, on by default), and posts one comment (kept up to date) with the report; the report also goes to the job summary. Other workflows of the same action: a weekly docs audit kept in one issue ([examples/staleproof-audit.yml](../examples/staleproof-audit.yml)), a weekly fix pull request (`mode: fix`, [examples/staleproof-fix.yml](../examples/staleproof-fix.yml)) and a release gate (`mode: release`, [examples/staleproof-release.yml](../examples/staleproof-release.yml)). Private repositories work with the workflow's default token. - **What it does not do.** It changes no files on the pull request's branch. The fix pull request is opened by the workflow's token only when the repository allows it: turn on **Allow GitHub Actions to create and approve pull requests** (Settings → Actions → General → Workflow permissions), or pass a token of your own as `github-token` ([Fix stale docs](fix.md#a-weekly-fix-pull-request)). Without `falkordb-url` it keeps nothing between runs. Pull requests from forks get a read-only token: the report lands in the job summary, not as a comment, and no suggestion is posted. Inputs, outputs and the comment format: [GitHub Action](github-action.md). ## Website: no install Open [staleproof.dev](https://staleproof.dev) and paste the URL of a pull request in a public GitHub repository, or open `https://staleproof.dev/#/try?pr=`, which starts the check directly. - **What it does.** The first check of a repository downloads it and indexes it on the fly into the workspace graph of its owner (`gh-`), together with any repositories you name next to it (a docs site, an SDK; up to 4 per workspace), then checks the pull request in graph mode and streams the trace: the impacted doc sections with their evidence paths, the cleared symbols and the ranking. Agent mode is offered when the server has a model key, within a daily budget. - **Audit a repository.** [staleproof.dev/#/audit](https://staleproof.dev/#/audit) audits a public repository with no pull request: every docs example checked against the code it calls, API coverage, a 0-100 health score and the README badge ([Docs audit](audit.md)). - **What it does not do.** It does not run examples: the hosted service has no sandbox. For FalkorDB's own repositories it shows the runs recorded earlier. It reads public repositories only, and leaves nothing on your repository: no comment, no pull request. ## Editors: while you type Install **Staleproof** (`staleproof.staleproof`) from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=staleproof.staleproof) in VS Code, or from [Open VSX](https://open-vsx.org/extension/staleproof/staleproof) in Cursor, Windsurf and VSCodium; from a shell, `code --install-extension staleproof.staleproof`. - **What it does.** The same findings as `staleproof lint`, from the unsaved text, with the verified fixes as quick fixes, and hover from a docs code span to the code. The extension runs `staleproof lsp`, a language server that Neovim, Zed, Helix and JetBrains IDEs can run too. - **What it does not do.** It reads the files, git and the local parse cache only: no graph, no network, no example runs. Setup for each editor: [Editors (LSP)](editors.md). ## Claude Code plugin In Claude Code: ```text /plugin marketplace add rajkaria/staleproof /plugin install staleproof@staleproof ``` - **What it does.** After each edit, Claude hears which docs and agent files the edit made wrong; before it stops, it is asked to fix them, once. The plugin also adds a skill and the commands `/staleproof:check`, `/staleproof:fix`, `/staleproof:audit` and `/staleproof:docs-for`. - **What it does not do.** Nothing leaves the machine: the hooks read the checkout and git. Without the plugin, `npx -y staleproof init --claude` (or `--cursor` for Cursor) writes the same hooks into the project's settings. Hooks, output and configuration: [Claude Code plugin](claude-code.md). ## MCP, hosted: no install The hosted MCP server speaks Streamable HTTP at `https://staleproof.dev/api/mcp`. Add it to your client: | Client | How | |---|---| | Claude Code | `claude mcp add --transport http staleproof https://staleproof.dev/api/mcp` | | Cursor | The **Add to Cursor** button on the install page, or `{"mcpServers": {"staleproof": {"url": "https://staleproof.dev/api/mcp"}}}` in `~/.cursor/mcp.json` | | VS Code | The **Add to VS Code** button on the install page, or `code --add-mcp '{"name":"staleproof","type":"http","url":"https://staleproof.dev/api/mcp"}'` | | Claude Desktop and claude.ai | Add a custom connector with the URL `https://staleproof.dev/api/mcp` | - **What it does.** `check_pull_request` returns the whole report for a pull request in one call; the graph tools behind it (`impact`, `read_section`, `decisions_for`, `rank_sections` and the rest) answer follow-up questions. Before a pull request, `docs_for_symbol` lists the docs that depend on a function and `check_changes` checks edited files; `audit_docs` audits a repository's docs. Any public repository is indexed on first use. - **What it does not do.** `run_example` is not offered and no example is executed. Private repositories cannot be read. ## MCP, local ```bash claude mcp add staleproof -- npx -y staleproof mcp ``` - **What it does.** Serves all fourteen tools over stdio, `run_example` included, from your machine. With no `FALKORDB_URL` it starts a local FalkorDB; the tools index the repository they are about the first time they see it. `check_changes` can read the working tree of your checkout directly. Private repositories work with `GITHUB_TOKEN` in the server's environment (`-e GITHUB_TOKEN=`). - **What it does not do.** It writes nothing to GitHub. The first start installs falkordblite, which takes a minute. Other clients, the tools and security notes: [MCP server](mcp.md). ## CLI: one command ```bash npx -y staleproof check https://github.com///pull/ --no-agent ``` - **What it needs.** Node.js 20 or newer, git, and uv (recommended) or `python3` 3.12 or newer for the local FalkorDB and the example runs. - **What it does.** With no `FALKORDB_URL` it starts a local FalkorDB (falkordblite, no Docker), indexes the pull request's repository at its default branch head into a graph named after the owner, checks the pull request, and runs up to 3 affected examples before and after it in a sandbox. Later checks of the same repository reuse the graph. Add `--with ` for docs that live in another repository, and set `GITHUB_TOKEN` for a private repository. - **Agent mode.** Without `--no-agent`, `check` runs the agent when model credentials are set: `ANTHROPIC_API_KEY`, or `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY` and `STALEPROOF_MODEL` for an Anthropic-compatible provider. Without them it prints a note and runs graph mode. - **Before a pull request, and with none.** `npx -y staleproof lint` checks the docs and agent files of a checkout in seconds, with no graph and no network, and `npx -y staleproof fix` applies the fixes it can verify; `npx -y staleproof check` with no URL checks your uncommitted and unpushed changes (`--fail-on-predicted` for a pre-push hook); `npx -y staleproof audit ` audits the docs as they are. - **What it does not do.** It opens no pull request unless you run `patch --open-pr` or `fix-pr`. The full walkthrough: [Quickstart](getting-started.md). Every command: [CLI](cli.md). ## pre-commit: on every commit In `.pre-commit-config.yaml`, with `rev` set to the latest release tag (the page linked below has it filled in): ```yaml repos: - repo: https://github.com/rajkaria/staleproof rev: v hooks: - id: staleproof ``` Then `pre-commit install`. - **What it does.** On `git commit`, it lints what the commit contains (the git index) against `HEAD` and fails when the staged changes make a docs reference wrong: a renamed script the README still runs, a link `AGENTS.md` still follows to a page that moved. An optional `staleproof-predicted` hook on `git push` fails when a docs example is predicted to break. - **What it does not do.** It reads only the local clone; nothing is fetched or sent. lefthook, husky, a plain git hook and why the hooks run the published package: [Pre-commit hooks](pre-commit.md). ## `staleproof init`: set it all up ```bash npx -y staleproof init ``` - **What it does.** Detects the GitHub repository, languages, docs folders, agent files and a separate docs repository the README links to, then offers each file and writes the ones you accept: `.staleproof.toml`, the workflow (the pull request check plus a weekly audit), the docs health badge in the README, the pre-commit (or lefthook, or husky) hook, `.staleproof/` in `.gitignore`, and the extension recommendation in `.vscode/extensions.json`. `--claude` and `--cursor` add the agent hooks. - **What it does not do.** `--dry-run` prints every file and change and writes nothing; run again, it leaves what is in place alone. Every option: [CLI](cli.md#init). ## Related - [Quickstart](getting-started.md) - [GitHub App](github-app.md) - [GitHub Action](github-action.md) - [Editors (LSP)](editors.md) - [Claude Code plugin](claude-code.md) - [Pre-commit hooks](pre-commit.md) - [MCP server](mcp.md) - [CLI](cli.md) - [Deploy](DEPLOY.md): run the hosted service yourself --- # Use cases Nine ways teams use `staleproof`, each with the situation it fits, what it does there, the setup, what you get and its limits. Read the one closest to your team; every setup links to the reference page with the full options. The running examples are FalkorDB's client library (`FalkorDB/falkordb-py`), the SDK built on it (`FalkorDB/GraphRAG-SDK`) and its docs site (`FalkorDB/docs`). ## SDK and client-library teams with a separate docs site **The situation.** Your client library, the SDK built on it and the docs site live in different repositories. A change in the client can make a docs page false without naming anything the page mentions: the page calls an SDK method, the SDK method calls the client, and the client function changed. Reviewers of the client pull request never see that page. **What it does.** One graph spans all your repositories. Calls are resolved across them through imports, types and re-exports, and docs are linked to the code their prose mentions and their examples call. For a pull request in the client, [impact](tools.md#impact) follows the call edges into the SDK and on to the docs site, and gives the evidence path for every section it reaches. When it runs an example, it installs every repository the example needs from source at the right commits, using the dependencies the repositories declare on each other. **Setup.** Index the repositories together, then check pull requests from any of them: ```bash npx -y staleproof index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/276 --no-agent ``` To get a comment on every pull request in the client repository, add the [GitHub Action](github-action.md) with the other repositories in `also-index`, so the job indexes them together. **What you get.** On [falkordb-py#276](https://github.com/FalkorDB/falkordb-py/pull/276), 4 changed symbols reach 43 doc sections, 41 of them in the other two repositories, through paths such as: ```text AsyncGraph.query ← FalkorDBConnection.query (GraphRAG-SDK) ← GraphStore.delete_document_chunks ← GraphRAG.delete_document ← GraphRAG-SDK README ``` A whole-word grep for the changed names finds none of those 43 sections and matches 8 others. Of the 21 examples tried before and after #276, 10 pass both times, 6 fail both times (already broken, not by this pull request), 3 cannot be installed together at those commits, and 2 are skipped (placeholders to fill in; not Python). The 3 that cannot be installed show the reason: `dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2`, because the SDK pins the client below the version under test. All figures are from [Facts from the graph](FACTS.md). **Limits.** - Code is read as Python, Go, or TypeScript and JavaScript in published npm packages; Python, JavaScript and TypeScript examples run, Go examples are checked only. - Calls and doc links cross repositories only between repositories indexed in the same `index` command. Indexing or re-indexing one repository alone drops its cross-repository edges. - Example runs install today's third-party dependencies, and version skew between repositories (an SDK pinning an older client) is recorded as an install error, not a result. - Edges resolved by name rather than through types are labelled `by-name`; a path made of them is weaker evidence than an `exact` one. ## DevRel and docs teams after a release **The situation.** A release merged dozens of pull requests across your repositories. You need to know which pages are now wrong, fix the most-read ones first, and send the fixes as reviewable pull requests. **What it does.** Staleproof checks each merged pull request. In agent mode a model judges every impacted section and proposes replacement text for the ones that are wrong; each proposal is refused unless the graph has a path from the change to the section. [rank_sections](tools.md#rank_sections) orders sections by the PageRank of their page in your docs' own link graph, so the most central page comes first. `patch` turns a pull request's proposed edits into one diff across every docs repository it touches, with the evidence path, the example runs and any cited decisions for each edit, and `--open-pr` opens it as a pull request in your fork. **Setup.** List the release's merged pull requests (here with the GitHub CLI), check each one with the agent, then build and open the patches: ```bash prs=$(gh pr list --repo FalkorDB/falkordb-py --state merged --search "merged:>=2026-10-01" --json url --jq '.[].url') for pr in $prs; do npx -y staleproof check "$pr"; done for pr in $prs; do npx -y staleproof patch "$pr" --out "docs-$(basename "$pr").diff"; done ``` Review the diffs, then open the ones you want (needs `GITHUB_TOKEN`): ```bash npx -y staleproof patch https://github.com/FalkorDB/falkordb-py/pull/ --open-pr ``` `--open-pr` forks each docs repository into your account, creates a branch named after the code pull request (for example `staleproof/FalkorDB-falkordb-py-276`) at the commit the graph indexed, commits the edited files and opens the pull request inside your fork. Add `--upstream` to open it against the original repository instead. For ranking, graph mode prints a pull request's sections in PageRank order, and the GitHub Action comment uses the same order. To rank a whole release at once, pass the section ids from several checks to `rank_sections` through an [MCP client](mcp.md) or the [library](library.md). **What you get.** Per pull request: a `git apply`-able diff, a pull request description with the reason and evidence path for each edited section, the sections that were checked and left unchanged with their verdicts, and the sections that could not be applied. **Limits.** - There is no release-level command: you check and patch one pull request at a time, and each gives its own patch and its own docs pull request per docs repository. - Only agent mode proposes edits. Graph mode writes no verdicts, so its patch is empty. - No edit has been proposed on FalkorDB's repositories yet: the pull requests checked there are fixes, and every verdict on #262, #267 and #276 is `ok`. Edits and the patch are exercised by the test suite on a fixture organisation. - An edit is not applied when its section's text changed since the docs repository was indexed, or when the file is gone. Sections longer than 8,000 characters always conflict, because the graph stores only their first 8,000 characters. - `--open-pr` always creates or reuses a fork, even with `--upstream`. - The ranking in graph-mode output and in the comment covers the first impact page (40 sections). ## Open-source maintainers reviewing pull requests **The situation.** Contributors send pull requests that change behaviour. You want to know, during review, which docs pages a change can falsify, without paying for a model on every pull request. **What it does.** The [GitHub Action](github-action.md) runs on every pull request and posts one comment, updated in place on each run. It starts a FalkorDB in the job, indexes the repository, and runs up to 3 affected examples at the commit before the pull request and at the pull request. The comment leads with what broke: the examples run before and after, then the impacted sections with the change type, repository, evidence path and hops, the changed symbols no doc reaches (each with a `git grep` command to check the claim), and any verdicts. Without a model key it runs graph mode; with one, the agent judges each section and the comment adds its verdicts and summary. **Setup.** Click **Add to owner/repo** on [staleproof.dev/#/install](https://staleproof.dev/#/install) and commit the file GitHub opens, or add the workflow as `.github/workflows/staleproof.yml` yourself. No secrets, no database: ```yaml name: Staleproof on: pull_request: permissions: contents: read pull-requests: write jobs: docs: runs-on: ubuntu-latest steps: - uses: rajkaria/staleproof@v1 with: also-index: FalkorDB/docs # docs in another repository (optional) fail-on: breaks # fail the check when an example passes before and fails after (optional) ``` **What you get.** A comment that starts with the hidden marker ``, titled with what broke (`this pull request breaks 1 docs example`, or how many sections it can make false), the examples table, the docs it can make false with the path for each, and the graph name and commits compared at the end. The same report goes to the job summary. [GitHub Action](github-action.md#the-comment) shows the full format. **No workflow file.** The [GitHub App](github-app.md) gives the same review without YAML, on public and private repositories: a **Staleproof** check run with the docs references the pull request makes wrong as annotations, the comment when there is something to say, each verified fix inside the diff as a suggestion, and an **Apply fixes** button for the rest. It runs no examples; keep the Action for the before and after runs. The two can run together without posting the same suggestion twice. **Limits.** - The action has run end to end on a private test repository with the one-click workflow (no inputs). - Graph mode samples: it runs at most 3 examples (the `runs` input), from exactly linked sections on the first impact page, and writes no verdicts. - With the default FalkorDB nothing carries over between runs: no decisions, verdict history or recorded runs. Set `falkordb-url` to keep them; an open pull request is then diffed again whenever new commits are pushed. - Some rows overstate the change: moving a file reports its definitions as removed, and a module that stops importing a name from its own package reports a removed export, both labelled breaking. - Pull requests from forks get a read-only token and no secrets: the check runs in graph mode and its report goes to the job summary, but no comment is posted. - Both modes install and run code from the pull request on the runner (the examples), with a guarded environment; see the [security notes](github-action.md#security-notes). ## A weekly docs audit **The situation.** Docs drift between releases too, not only in the pull request that changed the code: an SDK renames a parameter, a docs site in another repository keeps the old call, and nobody runs the page again. You want a standing report of what is wrong now, and a number that says whether it is getting better. **What it does.** The [docs audit](audit.md) checks every docs example against the code it calls, across repositories, without running it: unknown keywords, wrong argument counts, names that no longer exist. It adds the runs of up to 20 examples at the default branch and the share of the public API no doc covers, and scores it from 0 to 100. On a schedule the GitHub Action keeps one issue up to date with the report: opened when an example no longer fits or fails, updated on every run, closed once all is clear. **Setup.** Add [examples/staleproof-audit.yml](../examples/staleproof-audit.yml) as `.github/workflows/staleproof-audit.yml` (or tick the weekly audit option on [staleproof.dev/#/install](https://staleproof.dev/#/install)): ```yaml name: Staleproof docs audit on: schedule: - cron: '0 6 * * 1' workflow_dispatch: permissions: contents: read issues: write jobs: audit: runs-on: ubuntu-latest steps: - uses: rajkaria/staleproof@v1 with: also-index: FalkorDB/docs # docs in another repository (optional) ``` To see a first report without CI, audit on [staleproof.dev/#/audit](https://staleproof.dev/#/audit) or run `npx -y staleproof audit `. Put the badge the report prints in your README. **What you get.** Lines like these, each linked to the line in the docs (all found on the current docs of real projects; [more](audit.md#what-it-finds-on-real-repositories)): - the FalkorDB docs page `commands/graph.constraint-create.mdx` calls `client.create_constraint`, which `FalkorDB` does not have; - GraphRAG-SDK's `docs/api-reference.mdx` documents `VectorStore(index_name=..., similarity_function=...)`, parameters the constructor no longer has; - Flask's `docs/views.rst` calls `UserList.as_view()` without its required `name`. Then the failing runs, the most used undocumented API, and the score with each deduction. **Limits.** - Static checks judge only calls resolved through imports and types; a clean audit is not proof that every example works. The runs cover the rest, for Python, JavaScript and TypeScript. - Go examples get name and argument checks but are never run. - Coverage counts a symbol as documented when any doc names or calls it, however briefly. - The issue tracks examples that no longer fit or fail; low coverage lowers the score but opens no issue on its own. ## Developers, before they push **The situation.** You are changing a function in a library whose docs, maybe in another repository, call it. You want to know which examples your change breaks before you push, not from a reviewer or a reader later. **What it does.** `staleproof check` with no pull request URL diffs your checkout's uncommitted and unpushed changes against its merge base, walks the graph from the changed symbols to every doc that reaches them, and predicts which examples break: a removed or renamed name, an import that stops existing, a call the new signature rejects. Nothing runs and nothing is written to the graph. `staleproof docs-for ` answers the question before you start: which docs depend on this function. **Setup.** Once, in the checkout (the first run indexes the repository; add `--with ` when the docs live elsewhere): ```bash npx -y staleproof docs-for Graph.query --repo FalkorDB/falkordb-py npx -y staleproof check ``` As a pre-push hook that stops the push when an example is predicted to break: ```bash printf '#!/bin/sh\nnpx -y staleproof check --fail-on-predicted\n' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push ``` With no graph at all, in a second: `npx -y staleproof lint --new` lists the docs references your changes make wrong in the docs, READMEs and agent files (a renamed script the README still runs, a moved file `AGENTS.md` still names), and `npx -y staleproof fix` applies the fixes it can verify. The [pre-commit hook](pre-commit.md) runs the same lint on every commit, and the [VS Code and Cursor extension](editors.md) shows it as you type. **What you get.** The impacted sections with their evidence paths, then the examples predicted to break, each with its line and the reason, for example: ```text Predicted to break (1): the call no longer fits the changed code ✗ docs/getting-started.mdx:0 line 18 g.query `Graph.query` requires `timeout`, which the example does not pass ``` (from the test suite's fixture organisation, after adding a required `timeout` to `Graph.query`), and the impacted lines that were already out of date. [CLI](cli.md#check-without-a-pull-request) has the full output. **Limits.** - It is static: it predicts breaks, it does not run examples. Open the pull request for the before/after runs. - The docs' repository must be in the graph with the code; index them together (`--with`) the first time. - Changes the static check cannot judge (a decorated function, a call with `**kwargs`) are listed as impact without a prediction. ## AI coding agents over MCP **The situation.** You change code with an AI coding agent such as Claude Code, and want the same session to find and fix the docs the change affects before a human reviews it. **What it does.** The MCP server gives the agent the Staleproof tools: `check_pull_request` returns the whole report for a pull request in one call, and the graph tools let it read the sections, run the examples before and after, check maintainer decisions, record verdicts or propose edits, and get the docs patch as a diff to apply locally. Every result includes the Cypher queries it ran. **Setup.** With nothing installed, the hosted server (public repositories, no example runs): ```bash claude mcp add --transport http staleproof https://staleproof.dev/api/mcp ``` Or the local server over stdio, which runs examples and reads private repositories with `GITHUB_TOKEN`; it starts a local FalkorDB and indexes each pull request's repository on first use: ```bash claude mcp add staleproof -- npx -y staleproof mcp ``` Cursor, VS Code and Claude Desktop have one-click buttons or a URL to paste on [staleproof.dev/#/install](https://staleproof.dev/#/install). Prompts to try, after pushing your branch and opening the pull request: - "What docs can https://github.com/FalkorDB/falkordb-py/pull/267 falsify? Show the evidence path for each section." - "Run the examples in those sections before and after that pull request and tell me which ones it breaks." - "Which of those sections is most central in the docs?" - "Check the maintainer decisions for each section, then record a verdict for the ones that still hold and propose an edit for the ones that don't." - "Give me the docs patch for that pull request and apply it to my checkout of FalkorDB/docs." [MCP server](mcp.md) walks through a full session. **What you get.** Answers grounded in the graph, with the evidence path and Cypher behind each, verdicts and proposed edits stored on the graph for the next check, and a diff you can apply. **Limits.** - `check_pull_request` and the graph tools take a GitHub pull request URL; for edits not pushed yet, see [the next use case](#coding-agents-before-the-pull-request). - The hosted server reads public repositories only and does not offer `run_example`. - First-use indexing indexes the pull request's repository alone; index a docs site that lives elsewhere together with it first (`staleproof index `). - `propose_edit` and `record_verdict` need `pr_url` over MCP; there is no pull request "under check" to default to. - One local MCP server serves the graph named by `STALEPROOF_ORG`, else the graph of the owner of the repository each call names. Register a second server with another `STALEPROOF_ORG` to pin another graph. - `run_example` installs repositories and runs docs code on the machine running the local MCP server. See the security notes in [MCP server](mcp.md). ## Coding agents, before the pull request **The situation.** An AI coding agent edits your library. Docs should change in the same commit as the code, but the agent does not know which pages call the function it is changing, and nothing tells it until a human reviews the pull request. **What it does.** Two MCP tools work before there is a pull request. `docs_for_symbol` takes a function, class or method and returns every doc section and example that depends on it, with the evidence path, so the agent knows what to update. `check_changes` takes the files it edited (path and new content) and returns the docs they can make false and the examples predicted to break, compared with the commit the graph indexed; on a local server it can read the checkout's working tree instead. Neither writes anything. **Setup.** Add the MCP server ([hosted or local](mcp.md)), then put these rules in the repository's `AGENTS.md` or `CLAUDE.md` so the agent calls the tools unasked (the install page has them to copy): ```markdown ## Docs stay true to code (Staleproof MCP) - Before changing a public function, class or method, call docs_for_symbol with its name: update every doc section and example it lists in the same change. - Before committing, call check_changes with the files you edited: fix every example it predicts to break (its call no longer fits the new signature). ``` **What you get.** For a change that adds a required `timeout` to `Graph.query` in the fixture organisation: `docs_for_symbol` lists the 3 doc sections that reach it, and `check_changes` names the one example line that breaks (`` `Graph.query` requires `timeout`, which the example does not pass ``) with a link to it. [MCP server](mcp.md#before-a-pull-request) shows the calls. **Limits.** - `check_changes` is static: it predicts breaks and runs nothing. - The hosted server cannot read your disk: send `files` with `repo`. It reads public repositories only. - The rules are instructions to the agent, not enforcement; pair them with the [pre-push hook](#developers-before-they-push) or the GitHub Action. **In Claude Code, with no MCP server.** The [Claude Code plugin](claude-code.md) (`/plugin marketplace add rajkaria/staleproof`, then `/plugin install staleproof@staleproof`) needs no rules in `AGENTS.md`: after each edit Claude hears which docs and agent files the edit made wrong, with the verified fix when there is one, and before it stops it is asked to fix them, once. It reads the checkout and git only. In Cursor, `npx -y staleproof init --cursor` writes the same hooks. ## Platform teams with many repositories or organisations **The situation.** You run docs tooling for several product lines or several organisations, and want one service rather than one setup per team. **What it does.** Each organisation gets its own graph on the same FalkorDB server; the graph name is the organisation slug. The CLI defaults to the graph of the repository's owner; `--org` or `STALEPROOF_ORG` names another, and the MCP server and the HTTP server use `STALEPROOF_ORG`, else `falkordb`. `serve --hosted` goes further and gives every repository owner a workspace graph of its own on demand, as staleproof.dev does. A second graph built from [encode/httpx](https://github.com/encode/httpx) sits next to `falkordb` on the same server: one repository, 544 symbols, 182 doc sections and 194 code examples ([its facts](FACTS-encode.md)). **Setup.** ```bash npx -y staleproof index encode/httpx npx -y staleproof check https://github.com/encode/httpx/pull/ --no-agent ``` Both use the graph `encode`, the repository's owner. Serve the [HTTP API](http-api.md) once per organisation (without `--hosted` the server answers for one graph), on separate ports: ```bash STALEPROOF_ORG=falkordb npx -y staleproof serve --port 8080 STALEPROOF_ORG=encode npx -y staleproof serve --port 8081 ``` Or embed the [library](library.md), which can open any graph on the server from one process: ```ts import { GraphStore, loadConfig, runGraphCheck } from 'staleproof'; const config = loadConfig(); const store = await GraphStore.connect(config.falkordbUrl); try { console.log(await store.listGraphs()); const graph = store.org('encode'); const result = await runGraphCheck({ graph, config }, 'https://github.com/encode/httpx/pull/', { maxRuns: 0 }); console.log(result.status, result.summary); } finally { await store.close(); } ``` **What you get.** Isolated graphs per organisation on shared infrastructure, each with its own checks, verdicts, decisions and runs. **Limits.** - Graphs do not link to each other: a dependency between organisations is not followed. - One `serve` process answers for one organisation (or, with `--hosted`, for every owner from one FalkorDB server, within the [hosted limits](DEPLOY.md#limits)) and runs one check at a time; further checks queue. - The HTTP API has no authentication. Its POST endpoints (checks, indexing, decisions, verdict reviews) are rate-limited per client, 30 an hour by default, and nothing more; put it behind your own access control. - GitHub repositories only. Private ones need `GITHUB_TOKEN`; a hosted server indexes whatever its token can read, so give it a token limited to public repositories. ## Recording maintainer decisions so docs stop being re-flagged **The situation.** Some docs are intentionally as they are: the async client keeps an old keyword argument until 2.0, or a page documents the previous release on purpose. Without memory, every check flags them again. **What it does.** A decision is a sentence, its source and author, and the doc sections or symbols it governs. Before judging a section, the agent calls [decisions_for](tools.md#decisions_for), which finds decisions that govern the section directly or govern a symbol within 2 call hops of what the section mentions or its examples call. A decision on one function therefore covers the docs of the code that calls it. The agent then records a `governed` verdict that must cite the decision, instead of proposing an edit. A newer decision can supersede an older one; lookups resolve to the active successor. Maintainers can also accept or reject each verdict, and the agent sees earlier verdicts and their review status on the next check. **Setup.** Find the ids to govern: section ids come from impact (`check --json`), and a symbol id is `/:`, the qualified name shown in evidence paths. Then record the decision: ```bash npx -y staleproof decide "The async client keeps positional kwargs until 2.0" \ --governs "FalkorDB/falkordb-py:falkordb.asyncio.falkordb.FalkorDB.__init__" \ --source https://github.com/FalkorDB/falkordb-py/issues/ --author "" ``` It prints `Recorded decision dec-… governing 1 node(s).` To replace it later: ```bash npx -y staleproof decide "The async client drops positional kwargs in 2.0" \ --governs "FalkorDB/falkordb-py:falkordb.asyncio.falkordb.FalkorDB.__init__" \ --supersedes dec- ``` Accept or reject a verdict the agent wrote (rejected edits are left out of the patch): ```bash npx -y staleproof verdict reject ver- --by "" ``` **What you get.** Fewer repeat flags in agent mode, a `governed` verdict that names the decision, and a record of who decided what and where. **Limits.** - Decisions inform the agent's judgement only. Impact still lists a governed section, and graph mode and the graph-mode comment do not consult decisions. - Citing the decision is the model's job, guided by its instructions; the graph enforces only that a `governed` verdict cites at least one decision id. - `decide` refuses when none of the `--governs` ids exists in the graph. An unknown `--supersedes` id is ignored without an error. - Decisions come from people: the agent is never given the tool that writes them. ## Related - [Quickstart](getting-started.md) - [CLI](cli.md) - [GitHub Action](github-action.md) - [MCP server](mcp.md) - [Decisions and memory](memory.md) --- # Docs audit A docs audit answers "how true are these docs right now?" with no pull request: every call in a docs example checked against the code it reaches (in its own repository or another), every path, link, command and env var the docs, READMEs and AI agent files (`AGENTS.md`, `CLAUDE.md`, Cursor rules) name that the repository does not have, the latest example runs at the indexed commits, and how much of the public API the docs name or call, summed up as a health score from 0 to 100. This page covers what the audit checks, how the score is computed, the CLI command, the web page, the README badge, the HTTP API and MCP tool, the GitHub Action's scheduled audit, how to read and act on the report, and its limits. It is for maintainers and docs teams who want a baseline before relying on pull request checks, or a weekly report after. ## Run it ```bash npx -y staleproof audit FalkorDB/falkordb-py ``` With no FalkorDB configured the CLI starts a local one, indexes the repository at its default branch head if the graph does not have it, and prints the report. Name every repository that shares the docs, so calls across them resolve: `staleproof audit acme/acme-client acme/acme-docs` indexes the two together on first use. With no repository at all, `staleproof audit` audits every repository in the graph. Other ways to run the same audit: the page [staleproof.dev/#/audit](https://staleproof.dev/#/audit) for any public repository, [`GET /api/audit`](http-api.md#get-apiaudit), the MCP tool [`audit_docs`](tools.md#audit_docs), and the [GitHub Action](github-action.md#audit-the-docs-on-a-schedule) on a schedule. ## What it checks ### Docs references When a repository is indexed, [`staleproof lint`](lint.md) runs on the checkout and every finding is stored in the graph as a `Finding` node (placed in the doc section that holds it with an `IN_SECTION` edge), so the audit reads them later without a checkout. They are the references that no longer match the repository: missing paths and line numbers, broken links and anchors, `npm run`/`make`/`just` commands no script defines, unknown subcommands and flags of the repository's own CLI, documented env vars no code reads, and docstrings that document parameters a function lacks ([Lint rules](rules.md) lists every rule). The report lists them **AI agent instructions first**, then READMEs and contributing guides, then the docs, each with its line, the message and the rule: ```text Docs references that no longer match the repository (3: 3 errors) AI agent instructions AGENTS.md:6 error `docs/setup.md` does not exist; it moved to `docs/guides/setup.md`. [path-missing] AGENTS.md:5 error `lint:docs` is not a script in package.json. [command-missing] README and contributing guides README.md:5 error The link target `docs/setup.md` does not exist. [link-broken] ``` Example findings (`example-*` rules) are not stored twice: the examples section below is the indexer's own `MISMATCH` edges. A repository indexed by Staleproof 0.4 or later (index version 4) has its references checked; an older graph is re-indexed on next use on the hosted service, and with `staleproof index` elsewhere. ### Examples that no longer fit the code At index time every docs example is checked statically against the signatures and names its calls reach, and each line that does not fit becomes a `MISMATCH` edge in the graph ([Indexing](indexing.md#static-checks-of-examples) has the rules). The audit lists them, with the doc line, the section, the call as written and what is wrong: | Kind | Label in the report | Example detail | Languages | |---|---|---|---| | `unknown-keyword` | unknown keyword argument | `` `Client` has no parameter `hostname` `` | Python | | `too-many-arguments` | too many positional arguments | `` `Graph.query` takes at most 2 positional arguments; the example passes 3 `` | Python | | `missing-argument` | missing required argument | `` `Graph.query` requires `timeout`, which the example does not pass `` | Python | | `missing-import` | imports a name that does not exist | `` `miniclient` has no `connect` `` | Python, JavaScript, TypeScript | | `missing-attribute` | calls an attribute that does not exist | `` `Graph` has no attribute `read_only` `` | Python, JavaScript, TypeScript | The details above come from the test suite's fixture docs. Nothing runs to find them, so they cost no sandbox, work on the hosted service, and cover examples that could never run (an LLM key, a placeholder host). A check is only made when the source settles it: a call reached by name rather than through imports and types, a decorator that may rewrite the signature, a class with an external base or a `__getattr__`, or a `*args`/`**kwargs` splat leaves the line unjudged. ### Example runs The audit reads the latest recorded run of each example at the indexed commits (the `current` side; runs before and after a pull request are not counted), and only runs whose installed commits are the ones the graph holds now. `staleproof audit --run ` first runs up to `n` Python, JavaScript and TypeScript examples in the [sandbox](example-runner.md): those with a mismatch first, then the ones that call the most code. Examples marked `skip` or `expect-error` ([directives](indexing.md#example-directives)) are never run. Without `--run`, the audit uses only what earlier runs recorded. ### API coverage The public API is what a repository's packages export: - **Python**: names bound in a public package's `__init__.py` (or a top-level module) that resolve to the organisation's definitions, following `from .x import *` and imports from private modules (`httpx._api.get` counts as `httpx.get`), plus the public methods of exported classes and of their base classes in the organisation. - **TypeScript and JavaScript**: what each published package's entry file exports, followed through re-exports, plus the public methods of exported classes. A repository indexed before export information existed, or one whose package has a module-level `__getattr__` (lazy exports the source does not show), falls back to the Python naming rule: no part of the qualified name starts with `_`. Either way, symbols nested in functions and files under `tests/`, `scripts/`, `examples/`, `docs/`, `benchmarks/`, `tools/` and similar are left out. A public symbol is documented when a doc section names it in inline code or an example calls it (a `MENTIONS` or `USES` edge). A class also counts as documented when any of its methods is, so `g.query(...)` in an example documents `Graph`. Coverage is the documented share, to one decimal place. ### Undocumented API, most used first The public symbols no doc names or calls, ordered by how much other code depends on them: by the number of other repositories that call them, then classes before functions and methods, then by the number of symbols that call them. The first 25 are listed (`--limit`). ## The score The score starts at 100 and loses points for four things: | Deduction | Points | Cap | |---|---|---| | Each example with at least one line that no longer fits the code (a stable check's, or a beta check the repository opts in) | 8 | 50 | | Each other example that fails when run (`fail` or `timeout`; examples already counted as mismatched are not counted twice) | 6 | 30 | | **Docs references**: each error-level finding in the docs, READMEs and AI agent files (a missing path, a broken link, an undefined command); each warning (a missing anchor, a line past the end of a file) costs 1; info findings (an env var the env file does not list) cost nothing | 3 | 25 | | Each percent of public API that no doc names or calls | 0.3, rounded | none | The score never goes below 0. The grade is `A` from 90, `B` from 80, `C` from 70, `D` from 60, else `F`. Each deduction is listed in the report with its reason. Beta rules' findings and example lines (the **Maturity** column of the [rules reference](rules.md)) are listed apart, under "Lower confidence", and never deducted, unless the repository opts the rule in at `warning` or `error` in `[rules]` ([Configuration](configuration.md#repository-configuration)): then they count like any other. The setting is read when the repository is indexed. For example, auditing the fixture repository `acme/mini-client` from the test suite: 4 of its 11 public symbols are named or called by no doc, 63.6% coverage (−11, from 36.4 × 0.3). Its 4 example lines that no longer fit the code, in 2 examples, come from beta checks, so they are listed and cost nothing. The score is 89, grade B. With `[rules] "example-unknown-keyword" = "warning"` in the docs repository, the 2 examples with an unknown keyword would count (−16). A repository whose agent file names a moved file and whose README runs a renamed script loses 6 more: `Docs references: 2 errors where the docs or AI agent files name a path, link, command or setting the repository does not have (3 per error, 1 per warning, at most 25)`. The "Docs references" deduction arrived with Staleproof 0.5 (index version 4): a score computed before it can be up to 25 points higher for the same repository. ## What an audit covers | Called with | Repositories | Examples | Mismatches | API coverage | |---|---|---|---|---| | No repository | Every repository in the graph | Every live example | Every one | Every repository's symbols | | Repositories | Those | The examples in their docs, and examples in other repositories that call their code | In their docs, or in other docs against their code | Their symbols | So `audit acme/acme-client` also reports a line in `acme/acme-docs` that calls the client wrongly, when both are in the graph. A repository the graph does not have fails with `not indexed in the "" graph: ` (the CLI indexes it first unless `--no-index`). ## The CLI ```text staleproof audit [options] [repos...] ``` | Option | Default | Meaning | |---|---|---| | `--run ` | `0` | First run up to `n` Python, JavaScript and TypeScript examples at the indexed commits (needs `git`, and uv or Python 3.12+ for Python, Node for JavaScript and TypeScript) | | `--limit ` | `25` | How many undocumented symbols to list | | `--format ` | `text` | `text`, `markdown` (the Action's job summary and issue body) or `json` | | `--out ` | stdout | Write the report to a file; logs `wrote : ` on stderr | | `--fail-under ` | none | Exit 1 when the score is below this whole number | | `--fail-on-mismatch` | off | Exit 1 when any example line no longer fits the code | | `--no-index` | index on first use | Fail instead of indexing repositories the graph does not have yet | The [global options](cli.md#global-options) apply: without `--org`, the graph is the owner of the first repository named. [CLI](cli.md#audit) has the exit statuses. **Prints, text.** From the fixture `acme/mini-client` (colours off): ```text Docs health 89/100 (B) acme/mini-client@a6214ef Docs health 89/100 (B) for acme/mini-client. No example that calls the code is known not to fit it (5 checked); 4 lines from beta checks are listed apart, not counted. 7 of 11 public symbols (63.6%) are named or called by a doc. Score -11 4 of 11 public symbols are named or called by no doc (0.3 per missing percent) Examples that no longer fit the code (0 examples, 0 lines) none found by the stable checks Lower confidence (beta rules) Shown for review: these rules are still earning trust, so their findings never fail a check or lower the score. Opt a rule in with `[rules] "" = "warning"` in .staleproof.toml. ? docs/stale.md:0 line 8 · acme/stale-docs `miniclient` has no `connect` (imports a name that does not exist; miniclient.connect) ? docs/stale.md:0 line 10 · acme/stale-docs `Client` has no parameter `hostname` (unknown keyword argument; Client) ? docs/stale.md:1 line 19 · acme/stale-docs `Graph.query` takes at most 2 positional arguments; the example passes 3 (too many positional arguments; g.query) ? docs/stale.md:1 line 20 · acme/stale-docs `Graph` has no attribute `read_only` (calls an attribute that does not exist; g.read_only) False positive? Report it: https://github.com/rajkaria/staleproof/issues/new?template=false-positive.yml API coverage: 7 of 11 public symbols (63.6%) named or called by a doc Undocumented, most used first: · miniclient.asyncio.client.AsyncClient class · 1 callers (1 other repos) · acme/mini-client · miniclient.asyncio.client.AsyncClient.select_graph method · 1 callers (1 other repos) · acme/mini-client · miniclient.asyncio.client.AsyncGraph.query method · 1 callers (1 other repos) · acme/mini-client · miniclient.asyncio.client.AsyncGraph class · 1 callers · acme/mini-client Badge: [![docs health](https://staleproof.dev/api/badge/acme/mini-client.svg)](https://staleproof.dev/#/audit?repo=acme%2Fmini-client) ``` The mismatched examples live in another repository (`acme/stale-docs`) and call `acme/mini-client`'s code, so they are reported for it (and counted when `acme/stale-docs` opts their rules in). When the repository's docs references were checked, a section `Docs references that no longer match the repository (: , )` comes before the examples (see [Docs references](#docs-references)), or one line saying all of them match; the summary gains ` docs references in files no longer match the repository ( in AI agent instructions).` When runs are recorded, a section `Example runs at the indexed commits (

pass, fail, skipped, not installed)` lists the failing ones. The badge line appears when one repository is audited. When a summary sentence `Indexed by an older Staleproof: …` appears, see [Limits](#limits). **Prints, Markdown.** The same content as tables: the deductions, the docs references (one table per surface, AI agent instructions first, each row linked to the line: `| [AGENTS.md:5](…#L5) | `lint:docs` is not a script in package.json. | command-missing |`; the first 100, the JSON has all), the mismatched lines with links to the exact doc line (`| [docs/stale.md:1 line 20](…#L20) | Upgrading guide > Query | `g.read_only` | `Graph` has no attribute `read_only` |`), the failing runs, coverage with the undocumented symbols in a collapsed `

` block, the badge, a footer with the graph and commits, and two last lines the GitHub Action reads: ```text ``` The `audit-findings` line appears from Staleproof 0.5 on; the `audit` line keeps its shape. The first line is the marker ``, which the Action uses to find its issue. **JSON.** `{summary, org, repos: [{name, sha, symbols, sections, indexVersion, findingsChecked}], outdatedIndex, score, grade, deductions: [{points, why}], examples: {total, python, linked, mismatched}, mismatches: [{example, url, repo, path, heading, line, kind, detail, call, symbol, lowerConfidence?}], coverage: {api, documented, percent}, undocumented: [{qualname, repo, kind, signature, url, callers, crossRepoCallers}], runs: [{example, url, status, error, reason}], runCounts: {pass, fail, skipped, error}, findings: [Finding & {heading}], findingCounts: {errors, warnings, infos, byRule, bySurface, lowerConfidence?}, generatedAt}`. Each finding has the [lint JSON shape](lint.md#json-output) (`rule`, `severity`, `message`, `repo`, `path`, `line`, `column`, `endLine`, `endColumn`, `surface`, `subject`, `evidence`, `fix`, `fingerprint`) plus the `heading` of its doc section when it is in one; `findings` lists AI agent instruction files first. `findingsChecked` is false for a repository indexed before index version 4 or from a partial file list; the new fields are additive. `examples.linked` counts the examples that call at least one indexed symbol; `examples.mismatched` the ones with at least one counted `MISMATCH` line; `lowerConfidence: true` marks a beta check's line or finding (listed, never counted). ## On the web [staleproof.dev/#/audit](https://staleproof.dev/#/audit) audits any public GitHub repository: type `owner/name` (or a repository URL) and press **Audit**. The first audit downloads and indexes the repository into the workspace graph of its owner, showing the progress, then shows the score and grade, the deductions, the docs references that no longer match the repository (AI agent instructions first, each linked to its line), every example line that no longer fits (linked to the line on GitHub), failing runs recorded earlier, the coverage bar with the undocumented symbols, the badge Markdown (pointing at the host serving the page) and a link to install the GitHub Action. A link to a repository's report is `https://staleproof.dev/#/audit?repo=%2F`; opening it starts the audit. The hosted service runs no examples, so its audit scores the static checks and coverage only (plus runs recorded earlier, for FalkorDB's curated repositories). A server you run with `staleproof serve` serves the same page at `#/audit`; it audits the repositories in its graph, and with `--hosted` indexes any public repository first. ## The README badge ```markdown [![docs health](https://staleproof.dev/api/badge//.svg)](https://staleproof.dev/#/audit?repo=%2F) ``` The report prints this line for one repository. The badge reads `docs health | `, in the flat shields.io style: | Score | Colour | |---|---| | 90 and up | `#2ea44f` (green) | | 80 to 89 | `#97ca00` | | 70 to 79 | `#dfb317` (yellow) | | 60 to 69 | `#fe7d37` (orange) | | below 60 | `#e05d44` (red) | | not indexed on that server | `not audited`, grey | For an indexed repository the server computes the audit when the badge is requested (cached for 10 minutes per repository) and sends `Cache-Control: public, max-age=3600, s-maxage=3600`, so a README shows a score at most about an hour old. A repository the server has not indexed gets the grey badge, cached for 5 minutes: audit it once on the page (or `POST /api/audit`) and the badge fills in. The badge is served by any `staleproof serve`; replace the host to point it at yours. ## HTTP API and MCP | Interface | What it does | |---|---| | [`GET /api/audit?repo=`](http-api.md#get-apiaudit) | The audit of an indexed repository as JSON, cached for 10 minutes (`&fresh=1` recomputes it); 404 when it is not indexed | | [`POST /api/audit`](http-api.md#post-apiaudit) | Hosted mode: index the repository if needed, then audit it, streaming progress as server-sent events | | [`GET /api/badge//.svg`](http-api.md#get-apibadgeownernamesvg) | The badge | | MCP [`audit_docs`](tools.md#audit_docs) | The audit for an assistant: `repos` (default every repository in the graph) and `limit`. The stdio server indexes named repositories on first use; the hosted server indexes the first one and needs it named. It reads recorded runs and runs nothing | ## In CI: audit on a schedule The [GitHub Action](github-action.md#audit-the-docs-on-a-schedule) audits instead of checking a pull request when the workflow runs on a schedule, `workflow_dispatch` or `push`. Copy [examples/staleproof-audit.yml](../examples/staleproof-audit.yml): ```yaml name: Staleproof docs audit on: schedule: - cron: '0 6 * * 1' workflow_dispatch: permissions: contents: read issues: write jobs: audit: runs-on: ubuntu-latest steps: - uses: rajkaria/staleproof@v1 ``` Every Monday at 06:00 UTC it indexes the repository (and `also-index`), runs up to 20 examples at the default branch (`audit-runs`), writes the report to the job summary and keeps one issue up to date: opened when an example no longer fits or fails, updated on every run, and closed with a comment once all is clear. `fail-on: mismatches` and `fail-under: ` fail the job; the outputs `score` and `mismatched` carry the numbers. ## Owners of the docs to fix When the repository has a CODEOWNERS file (GitHub's lookup order: `.github/CODEOWNERS`, then the root, then `docs/`; the last matching pattern wins, a pattern with no owners leaves its paths unowned), the Markdown report (the Action's issue body) ends with **Docs to fix, by owner**: each owner with the references, example lines and failing examples on the pages they own, unowned pages last. Owners are written as code (`` `@acme/docs` ``), so nobody is notified; with ```toml [audit] mention-owners = true ``` in `.staleproof.toml` (or `[tool.staleproof.audit]`) they become @-mentions and GitHub notifies them. A pull request comment lists the **Owners of affected docs** the same way, from the CODEOWNERS file at the pull request's base commit. ## Weekly digest to Slack or Discord `staleproof digest` posts what changed since the last digest: the score and its change, the drift that appeared since then (with links to the lines), how many problems were fixed, and the verified fixes ready to apply. The payload follows the webhook's host: Slack incoming webhooks (`hooks.slack.com/services/...`) get `{text, blocks}`, Discord webhooks get `{content, embeds}` (with mentions disabled), anything else gets JSON (`{"type": "staleproof.digest", "text": ..., "score": ..., "new": [...], ...}`). ```bash STALEPROOF_DIGEST_WEBHOOK=https://hooks.slack.com/services/... npx -y staleproof digest acme/app ``` The previous digest is remembered as a hidden `` line: `--state-out` appends it to the audit report (the issue body), `--previous` reads it back, and `--graph-state` keeps it in a persistent graph instead. In the Action, set `digest-webhook: ${{ secrets.DOCS_DIGEST_WEBHOOK }}` on the scheduled audit: the state rides in the audit issue (open or closed). With no issue (nothing ever needed fixing) and no persistent graph, each digest compares with nothing and lists what is open. ## Reading the report Start with the docs references in AI agent instructions: every agent session reads them, so one stale path misleads every run. Then the mismatches: each one is a line a reader will copy and see fail. A docs reference names its fix in the message when there is one (`it moved to docs/guides/setup.md`); `staleproof lint` in the checkout shows the same findings, and `` silences one that is intentional ([Lint: configure and silence](lint.md#configure-and-silence)). | Kind | What to do | |---|---| | `unknown-keyword` | The parameter was renamed or removed. Find its new name in the signature the evidence points at (`staleproof docs-for ` shows the signature), or drop the argument | | `too-many-arguments` | A parameter became keyword-only, or was removed. Pass the rest by keyword, as the current signature names them | | `missing-argument` | A parameter became required. Pass it, or check whether the example should call something else | | `missing-import` | The name moved or was removed from the package. Import it from where it lives now, or use its replacement | | `missing-attribute` | The method was renamed or removed, or never existed on that class. Call the current method | Then the failing runs: a run that fails at the indexed commits is broken for readers now, whatever the static checks say (a wrong value, a server the page never sets up). Then coverage: the undocumented list is ordered so the symbols other code relies on most come first. When a finding is intentional, mark the example rather than editing the code: `` above an example that fails on purpose, `` above one that cannot be judged, or `skip-examples` in `.staleproof.toml` for a whole directory of versioned docs ([directives and repository config](configuration.md#repository-configuration)). Marked examples stay linked to the code (so pull request checks still find them) but are never judged or run. Re-index the docs repository after editing, then audit again. ## What it finds on real repositories Lines the static checks report on repositories as they are today (checked on 2026-10-17): - **FalkorDB docs**: `commands/graph.constraint-create.mdx` calls `client.create_constraint`, which `FalkorDB` does not have (`missing-attribute`). Running the example fails with `AttributeError: 'FalkorDB' object has no attribute 'create_constraint'`. - **GraphRAG-SDK**: `docs/api-reference.mdx` documents `VectorStore(index_name=..., similarity_function=...)`, parameters the constructor no longer has (`unknown-keyword`). - **Flask**: `docs/views.rst` calls `UserList.as_view()` without its required `name` (`missing-argument`), found in reStructuredText docs. - **httpx**: the docs pass `extensions=` to `httpx.get`, which no longer accepts it (`unknown-keyword`). ## Limits - **Static checks follow exact resolutions only.** A call whose receiver the types do not reveal (resolved by name, or not at all) is never judged, so a clean audit means "nothing found where the source is certain", not "every example works". Runs (`--run`, or the Action's `audit-runs`) cover the rest for Python. - **Guards leave lines unjudged**: functions with a decorator outside a short allow-list (`staticmethod`, `classmethod`, `functools.cache`, `lru_cache`, `abstractmethod`, `override`, `final`, `deprecated`, `wraps`), several definitions under one name (`@overload`), properties, classes with class decorators, a metaclass, a `__getattr__`, a `__new__` or a base class outside the organisation (other than `object`, `ABC`, `Generic`, `Protocol`), modules with a `__getattr__` or a star import from outside the organisation, calls with `*args` or `**kwargs`, and calls or signatures tree-sitter cannot read. - **TypeScript and JavaScript argument checks follow the language**: optional, default and rest parameters as TypeScript reads them, every parameter optional in JavaScript files; overloads, decorators and functions that read `arguments` are not judged. Go examples are checked (names, argument counts, packages) but not run. - **Runs need a sandbox.** `--run` needs `git` and uv or Python 3.12+ for Python examples, and Node with the repository's package manager for JavaScript and TypeScript ones ([JavaScript and TypeScript runs](example-runner.md#javascript-and-typescript)); the hosted service and `audit_docs` run nothing and read only recorded runs. - **Coverage counts references, not explanations.** A symbol named once in inline code counts as documented. - **An older index** (written before Staleproof 0.3) has no `MISMATCH` edges or export flags. The audit then adds `Indexed by an older Staleproof: examples are not checked against the code until the repositories are indexed again (staleproof index).` to its summary and sets `outdatedIndex`; re-index the repositories. Hosted workspaces indexed by an older version are re-indexed on their next use. - **One graph at a time.** An audit covers repositories in one graph; cross-repository findings need the repositories indexed together. - **Docs references need a full file list.** Lint judges paths against the whole tree, so the indexer lints a git checkout (the CLI and the Action) or a tarball whose every path it listed (the hosted service). A checkout from a partial download is not linted (`findingsChecked: false`) rather than reporting files it left out as missing. Findings are those of the indexed commit: run `staleproof lint` for the working tree. ## Related - [CLI](cli.md#audit): the `audit` command and its exit statuses - [Indexing](indexing.md#static-checks-of-examples): how examples are checked against the code - [GitHub Action](github-action.md#audit-the-docs-on-a-schedule): the scheduled audit and its issue - [Configuration](configuration.md#repository-configuration): directives and `.staleproof.toml` - [Core concepts](concepts.md#static-checks-and-the-audit) - [Checked stamp](verified-stamp.md): the audit's result as a one-line stamp on each docs page - [Release gate](release.md): breaking changes the changelog does not name, before a tag --- # Lint docs and agent files `staleproof lint` checks every claim your docs make about your repository against the repository itself: the paths they name, the links and anchors they follow, the commands they run (scripts, make targets, the repository's own CLI and its flags), and the code examples they show. It reads the files AI coding agents follow on every session first: `AGENTS.md`, `CLAUDE.md`, Cursor, Copilot, Windsurf and Cline rules. It needs no graph database, no network and no account, and it runs in about a second. ```bash npx staleproof lint ``` ```text AI agent instructions AGENTS.md 7:85 error The link target `docs/old-guide.md` does not exist. [link-broken] Lower confidence (beta rules) Shown for review: these rules are still earning trust, so their findings never fail a check or lower the score. Opt a rule in with `[rules] "" = "warning"` in .staleproof.toml. AGENTS.md 4:21 info `src/settings.ts` does not exist in the repository. [path-missing] 5:26 info `src/util/old.ts` does not exist; it moved to `src/helpers/old.ts`. [path-missing] 6:29 info `src/app.ts` has 4 lines; line 999 does not exist. [line-out-of-range] docs/guide.md 5:52 info This page has no heading `#configuration`. [anchor-missing] 7:70 info `lib/removed.ts` does not exist; it was deleted in 3f2a9c1. [path-missing] 9:56 info `docs/setup.md` has no heading `#legacy-usage`. [anchor-missing] False positive? Report it in one click: https://github.com/rajkaria/staleproof/issues/new?template=false-positive.yml (the rule, file:line and message are enough). 1 error, 6 info in 2 files (9 files checked, 111 ms); 6 of them lower confidence. ``` ## What it reads | Files | Report section | |---|---| | `AGENTS.md`, `AGENT.md`, `CLAUDE.md`, `CLAUDE.local.md`, `GEMINI.md` (any folder), `.claude/**/*.md` (skills, commands, agents), `.cursorrules`, `.cursor/rules/**`, `.github/copilot-instructions.md`, `.github/instructions/*.instructions.md`, `.windsurfrules`, `.windsurf/rules/**`, `.clinerules`, `CONVENTIONS.md` | AI agent instructions | | `README*`, `CONTRIBUTING*`, `DEVELOPMENT*`, `SETUP*`, `INSTALL*` | README and contributing guides | | Every other Markdown, MDX and reStructuredText page the indexer reads as docs | Docs | | Code blocks in those pages (Python, JavaScript, TypeScript, Go) | Code examples | | Docstrings and JSDoc comments of the repository's Python packages and published npm packages | Docstrings | | Env example files (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`, ...) and every code file's environment reads | Configuration | Changelogs, codes of conduct, licences, issue templates, translations (`docs/zh/…`, `i18n/de/…`), test fixtures and vendored folders are not checked. Add files with `[lint] include`, more agent files with `[lint] agent-files`. ## What it checks | Rule | Severity | Reports | |---|---|---| | `path-missing` | error | A code span names a file or folder that does not exist, with where it moved or when it was deleted | | `line-out-of-range` | warning | `path:line` or `path#L12` past the end of the file | | `link-broken` | error | A relative link to a missing file (and the right link when the file is at the repository root) | | `anchor-missing` | warning | `#anchor` with no heading of that slug in the target page | | `command-missing` | error | A command the repository does not define: `npm run x`, `pnpm x`, `yarn x`, `make x`, `just x`, `task x`, `uv run x`, `python -m pkg.mod` ([below](#commands-and-the-repositorys-own-cli)) | | `cli-command-unknown` | error | The repository's own CLI shown with a subcommand its definition lacks | | `cli-flag-unknown` | error | The repository's own CLI shown with an option its definition lacks | | `env-unused` | warning | A documented environment variable (an `.env.example` line, an env table, `export X=` in a shell block, a code span next to "environment variable") that no code reads ([below](#environment-variables-symbols-and-docstrings)) | | `env-undocumented` | info | Product code reads a variable that the repository's `.env.example` does not list | | `symbol-missing` | error | A code span `Client.query_v2`, `pkg.connect_async()` whose class or module is the repository's own and has no such member | | `docstring-param-unknown` | error | A docstring (Google, NumPy, Sphinx; JSDoc/TSDoc) documents a parameter the signature does not have | | `docstring-param-undocumented` | info | A docstring documents most parameters but leaves one out | | `example-*` | error | A docs example that calls the code with an unknown keyword, too many or too few arguments, a missing import or attribute, or a removed symbol ([static example checks](audit.md)) | The [rules reference](rules.md) lists every rule with an example. Beta rules (the **Maturity** column of the [rules reference](rules.md)) report at info under "Lower confidence (beta rules)" and never fail `--fail-on` or lower the audit score; `[rules] "" = "warning"` opts one in. A wrong finding? The report ends with a link to report it: see [False positives](false-positives.md). ## Precision first A finding you have to dismiss costs more than one Staleproof misses, so every rule tries to judge only what it can prove: - **Agent instruction files describe this repository**, so a path they name that does not exist is reported (at info, lower confidence: `path-missing` is a beta rule for now). Ordinary docs often describe a reader's project or another repository (`create src/app.py`, Flask's `docs/views.rst`), so there a missing path is reported only when git history shows this repository had it: it was deleted or renamed. The message says which, and where it went. - A path counts only when its first folder exists in the repository (or in history): `owner/repo`, `text/plain`, `feature/x` and `path/to/` are never paths. URLs, absolute and home paths, globs and placeholders are skipped. - Paths are tried from the repository root, the page's folder and every folder above it (skills and packages name files from their own folder). Paths git ignores, build outputs (`dist/`, `build/`, `.next/`, …) and paths the sentence says are created or generated are not reported. - Links: a docs site that routes extensionless links itself (Docusaurus, MkDocs, Mintlify, VitePress, …) is left to route them; folders such a site generates (an API reference) are not required to exist; a root page included in the docs site (`CONTRIBUTING.md`) may link relative to the docs folder. - Git history is read with one `git log` that never fetches file contents, so partial clones stay fast. On eight public repositories (openai/codex, vercel/ai, pydantic-ai, the MCP Python SDK, TypeScript, httpx, Flask, falkordb-py) it found real drift, such as headings renamed under deep links, test files moved under paths that `AGENTS.md` still names, or links to source files that a port renamed. On repositories it was never tuned on it still makes mistakes: the rules that make them most often are beta (above), and every false positive reported becomes a fixture that each release runs ([False positives](false-positives.md)). ## Commands and the repository's own CLI Commands are read from inline code spans and shell blocks (`bash`, `sh`, `console`, `zsh`, ...; unlabelled blocks whose lines start with `$ `) the way a shell reads them: prompts and output lines, `VAR=value` prefixes, `sudo` and `env`, `&&`/`||`/`;`/`|` chains, `\` continuations, comments and heredocs. A `cd` holds for the rest of the block, and a finding points at the command itself, not at the whole line. | Command | Checked against | |---|---| | `npm run x`, `npm test`/`start`/`stop`, `pnpm [run] x`, `yarn [run] x`, `bun run x` | `scripts` of the nearest `package.json` up from the page (or the `cd`), and the root's; `-w`/`--workspace`, `--filter`, `--prefix`, `-C`, `--dir`, `--cwd`, `yarn workspace x` pick the package | | `make x`, `make -C dir x` | Targets of `Makefile`/`makefile`/`GNUmakefile`, files they `include`, `.PHONY` lists; pattern rules (`release-%`) match | | `just x` | Recipes and aliases of the `justfile` (and its imports); `mod` names | | `task x` | Tasks and aliases of `Taskfile.yml`, included Taskfiles as `namespace:task`, wildcard tasks | | `uv run x`, `poetry run x`, `hatch run x`, `pdm run x` | `[project.scripts]`, `[tool.poetry.scripts]`, hatch environment scripts; `hatch run env:x` needs the environment | | `python -m pkg.mod` | Modules of the repository's own Python packages (`pkg/mod.py`, `pkg/mod/__init__.py`; a package needs `__main__.py`) | The repository's own CLI is the program a `package.json` `bin`, a `[project.scripts]` entry point or a package's `__main__.py` runs, however the docs invoke it: by name, `npx`/`pnpm exec`/`bunx`, `uv run`/`uvx`, `python -m pkg`, `node dist/cli.js` or `tsx src/cli.ts`. Its subcommands and options are read from the code that defines them, without running it: commander (and the subcommands of `.strict()` yargs programs) in JavaScript and TypeScript, click, typer and argparse in Python, across files (`addCommand(makeSync())`, `@cli.command()` in another module, `app.add_typer(users, name="users")`). `--help`, `-h`, `--version` and `-V` are always known, as are a parent command's options; values of options are skipped, `--x=value` and short flag clusters (`-qv`) are read, and argparse's abbreviations (`--verb` for `--verbose`) are accepted. What is never judged, because it cannot be proven wrong: - A definition that is not fully static: computed command or option names, plugin hooks (`pm.hook.register_commands(cli=cli)`), custom group classes (`cls=DefaultGroup`), unknown-option passthrough (`allowUnknownOption()`, `ignore_unknown_options`, `parse_known_args`), decorators or helpers that may add options, executable subcommands, other frameworks (cac, oclif, Cobra, clap, ...). Only the part that is open is skipped. - Names a dependency's binary or a file could have: `pnpm eslint`, `yarn run tsc`, `bun run index.ts`, `uv run pytest`. Through pnpm, yarn and bun only names with a colon (`build:docs`) and the usual script names (`build`, `lint`, `dev`, ...) are judged; through `uv run` and friends only a name close to one of the repository's own scripts (a rename). - Placeholders (`npm run ``` The script finds the page's source file, first match wins: 1. an element `
`: the stamp renders inside it; 2. ``; 3. the script tag's `data-path="docs/install.md"`; 4. the script tag's `data-map`: patterns filled with the URL path, tried in order: `data-map="docs/{path}.md docs/{path}/index.md"` maps `/guide/install/` to `docs/guide/install.md`, then `docs/guide/install/index.md`, and `/` to `docs/index.md`. `data-base="/project/"` drops a prefix from the URL path first (GitHub Pages project sites). Without a slot, the stamp goes into `data-target` (a CSS selector on the script tag), else right after the first `h1` in `article` or `main`, else at the end of the page. After client-side navigation, call `window.StaleproofStamp()`: it replaces the old stamp. ### Look The stamp is a small bordered line in the page's own font and text colour, so it fits light and dark themes. Only the icon and border are tinted (green, amber), with colours for dark mode from `prefers-color-scheme` and from the site's own theme switch (`[data-theme=dark]` in Docusaurus, `.dark`, Material's `slate` scheme). It is an `aside` labelled "Staleproof docs check"; the icon is hidden from screen readers (the words carry the state), the time is a `
(fetch it first; Staleproof never fetches for lint)`). ## In the GitHub Action `mode: release` runs the gate on the checked-out repository, writes the report to the job summary and fails the job per `release-fail-on`. Copy [examples/staleproof-release.yml](../examples/staleproof-release.yml): ```yaml name: Staleproof release gate on: release: types: [published] workflow_dispatch: inputs: from: description: Compare with this tag or commit (default the latest tag before the release) required: false default: '' permissions: contents: read jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: rajkaria/staleproof@v1 with: mode: release release-from: ${{ inputs.from }} ``` On `release: published` the checkout is the release's tag, so the gate compares it with the tag before. Inputs: `release-from`, `release-to`, `release-fail-on` (default `undocumented-breaking,stale-docs`), `changelog`. Outputs: `breaking`, `undocumented`, `stale-docs`. See [GitHub Action](github-action.md#release-gate). ## On real releases | Release | Breaking changes found | Hand check | |---|---|---| | encode/httpx 0.27.2 → 0.28.0 | 13: `proxies` and `cert` removed from `httpx.get`/`post`/... and `proxies`/`app` from `Client`/`AsyncClient` (in the changelog); `create_ssl_context` lost `http2` and swapped `cert`/`verify`, `HTTPTransport`/`AsyncHTTPTransport` moved `trust_env` before `http1` (not in the changelog) | All 13 true. The 3 undocumented ones are real: positional callers of those break. The changelog names `cert` as "now deprecated" for the top-level functions, where 0.28.0 removed it; the gate counts the mention (it does not judge meaning). | | encode/httpx 0.28.0 → 0.28.1 | 0 | True: a bug fix. | | pallets/flask 3.0.3 → 3.1.0 | 0 (11 compatible signature changes) | True: new optional parameters; the `max_content_length` setter is a property, not a call. | | tj/commander.js v12.1.0 → v13.0.0 | 1: `Help.wrap` removed, in the changelog | True ("`Help.wrap()` refactored into `formatItem()` and `boxWrap()`"). The other breaking items there are behaviour, which no signature shows. | | tj/commander.js v13.1.0 → v14.0.0 | 0 | True: the break is the Node.js version. | ## Limits - Signatures, not behaviour: a function that now raises on input it used to accept, or returns something else, is not seen. The changelog check covers only what the gate found. - One repository: docs in another repository are not checked against the release (run [`staleproof check`](cli.md#check) on the pull requests, or the [audit](audit.md) on the docs repository). - A name mentioned for another reason counts as mentioned, so a short, common name (`get`) can pass when the changelog says nothing about it. - Python's `__all__` is read when it is a literal list or tuple; one built at run time is not, and the module's public names count instead. ## Related - [Docs audit](audit.md): the docs as they are now, with no release. - [Lint docs and agent files](lint.md): the reference checks the gate runs at both commits. - [CLI](cli.md#release): every option. --- # MCP server Staleproof's tools over the Model Context Protocol, so Claude Code, Claude Desktop, claude.ai, Cursor, VS Code or any MCP client can ask what a pull request can make false, run the affected examples, record verdicts and get the docs patch; and, before there is a pull request, which docs depend on a function it is about to change, which docs its edits make false, and how the docs stand overall. Use the hosted server with nothing installed, or run `staleproof mcp` on your machine over stdio. This page covers both, what the server exposes, how results and errors look, a worked session and security notes. ## Hosted: nothing to install The hosted server answers at `https://staleproof.dev/api/mcp` over Streamable HTTP. It is stateless: every request gets a fresh server, there are no sessions, and responses are plain JSON. | Client | How | |---|---| | Claude Code | `claude mcp add --transport http staleproof https://staleproof.dev/api/mcp` | | Cursor | The one-click **Add to Cursor** button on [staleproof.dev/#/install](https://staleproof.dev/#/install), or this in `~/.cursor/mcp.json`: `{"mcpServers": {"staleproof": {"url": "https://staleproof.dev/api/mcp"}}}` | | VS Code | The **Add to VS Code** button on the install page, or `code --add-mcp '{"name":"staleproof","type":"http","url":"https://staleproof.dev/api/mcp"}'` | | Claude Desktop and claude.ai | Add a custom connector (Settings, Connectors) with the URL `https://staleproof.dev/api/mcp` | | Any other client | Its setting for a remote (Streamable HTTP) server, with that URL | Then ask, for example: "Which docs can https://github.com/FalkorDB/falkordb-py/pull/276 make false? Show the evidence path for each." On the hosted server: - **Any public repository is indexed on first use.** `check_pull_request`, `impact` and `clearance` index the pull request's repository when no graph has it, into the workspace graph of its owner (`gh-`), and so do `check_changes` (its `repo`), `docs_for_symbol` (the first of `repos`, or the `owner/repo:` prefix of the symbol id) and `audit_docs` (the first of `repos`). A workspace is indexed again at its default branch head once it is more than 7 days old, or when an older Staleproof indexed it. FalkorDB's own repositories answer from the curated `falkordb` graph. The limits are in [Deploy](DEPLOY.md#hosted-mode). - **Every other tool goes to the graph of the repository it names**: the `owner/repo:` prefix of its ids. A repository that is not indexed yet fails with ` is not indexed yet: call check_pull_request with one of its pull requests first`. A call that names no repository fails with `name a repository: pass a pull request URL (pr_url), repo or repos ("owner/name"), or ids that start with owner/repo:`, except `decisions_for` and `write_decision`, which then use the curated graph. - **No examples run.** `run_example` is not offered, because the service has no sandbox, and `check_pull_request` with `run_examples` executes nothing there. Run examples with the local server, the [CLI](cli.md#run) or the [GitHub Action](github-action.md). - **It cannot read your disk.** `check_changes` takes the edited files' contents (`files`); `dir` fails with `send the changed files: files: [{path, content}] with repo (this server cannot read your disk)`. `lint_docs` takes `repo` (any public repository, read from its tarball and not indexed); its `dir` fails the same way. ## Local over stdio `staleproof mcp` serves the tools on stdio from your machine, `run_example` included. ### Claude Code ```bash claude mcp add staleproof -- npx -y staleproof mcp ``` With no `FALKORDB_URL`, the server uses the FalkorDB on `localhost:6379`, and starts a local one there (falkordblite) when nothing answers. `check_pull_request`, `impact` and `clearance` index the pull request's repository the first time they see it, at its default branch head, so nothing has to be set up first. `audit_docs` and `docs_for_symbol` index the repositories named in `repos`; `check_changes` indexes the repository it checks: `repo`, or the `origin` remote of the checkout in `dir` (default: the directory the server was started in, usually your project). To use a FalkorDB elsewhere, or another graph: ```bash claude mcp add staleproof -e FALKORDB_URL='falkor://falkordb:@:' -e STALEPROOF_ORG=acme -- npx -y staleproof mcp ``` Use `falkors://` for an endpoint with TLS. Add `-e GITHUB_TOKEN=` for private repositories and to raise GitHub's limit of 60 API requests an hour, which the first look at each pull request uses. Check the server with `claude mcp list`. The stdio server uses the graph named by `--org` or `STALEPROOF_ORG` when one is set, else, like the CLI, the graph named after the owner of the repository each call is about (for `check_changes` on a checkout, the owner of its `origin`), so `check` and the MCP tools read the same graph. Repositories it indexes on first use go into that graph. ### Claude Desktop, Cursor and other clients Any client that launches a stdio server from a command, arguments and environment works. The configuration is the same everywhere: ```json { "mcpServers": { "staleproof": { "command": "npx", "args": ["-y", "staleproof", "mcp"], "env": { "STALEPROOF_ORG": "falkordb" } } } } ``` | Client | Where it goes | |---|---| | Claude Desktop | `claude_desktop_config.json`, opened from Settings, Developer, Edit Config. Restart the app after editing. | | Cursor | `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` in one project. | | Other clients | Their MCP server settings, as `command`, `args` and `env`. | Desktop apps often start without your shell's `PATH`. If the client reports that `npx` is not found, set `command` to its absolute path (the output of `which npx`). The tools that run examples also need `git`, `tar` and uv or `python3` on the `PATH` the server sees. For a second graph, add a second entry, for example `staleproof-encode` with `"STALEPROOF_ORG": "encode"`. ### Over HTTP from your machine `npx -y staleproof serve` exposes the same endpoint locally, at `http://127.0.0.1:8080/api/mcp`, for clients that only speak HTTP. Started plainly it answers from its one graph and indexes a repository into it the first time an indexing tool names it, as the stdio server does (but `check_changes` takes `files` there, not `dir`); `serve --hosted` indexes any public repository on demand into workspace graphs, as the hosted server does. See [HTTP API](http-api.md#post-apimcp). ## What the server exposes | Property | Value | |---|---| | Server name | `staleproof` | | Version | The package version (`staleproof --version`) | | Capabilities | Tools only | | Transport | stdio (`staleproof mcp`): stdout carries the protocol; the server logs `staleproof mcp: serving graph "" from ` (password masked) and indexing progress (`staleproof: `) to stderr. Streamable HTTP (`POST /api/mcp` of `staleproof serve` and of the hosted service): stateless, JSON responses; `GET` and `DELETE` answer 405. | | Startup (stdio) | Connects to FalkorDB first. With neither `FALKORDB_URL` nor `--db` set and nothing on `localhost:6379`, it starts a local FalkorDB; the first start installs falkordblite, which takes about a minute, so a client may report a timeout on the very first launch. With a URL set and the server unreachable, the process exits with `cannot reach FalkorDB at : …` and the client shows the server as failed. | The server gives clients these instructions: ```text Staleproof keeps docs true to code over a FalkorDB graph of an organisation's repositories. For a pull request, call check_pull_request first: it returns every doc section and example the change can make false, with the evidence path for each. To judge sections one by one: impact, read_section and run_example, decisions_for before editing, then propose_edit or record_verdict for every impacted section, and docs_patch for the diff. Before a pull request exists: docs_for_symbol says which docs depend on a function you are about to change, and check_changes says which docs your edits make false and which examples they break. With no change at all, audit_docs reports which examples no longer fit the code and which public API no doc covers. lint_docs needs no graph: it lists the paths, links, commands and env vars a repository's docs and AI agent files (AGENTS.md, CLAUDE.md) name that do not exist. Every graph result lists the Cypher it ran. ``` The hosted server appends: `This is the hosted server: any public GitHub repository is indexed on first use (into a workspace graph named gh-); examples are not executed here; run them with the CLI (npx -y staleproof run --pr ) or the GitHub Action.` ## The tools Fifteen tools over stdio; fourteen on the hosted server, which leaves out `run_example`. Each tool's description, as the client sees it, ends with the Cypher of the first query it runs; its input schema is JSON Schema. [Tools reference](tools.md) lists every parameter. | Tool | What it does | Writes to the graph | |---|---|---| | [`check_pull_request`](tools.md#check_pull_request) | The whole check in one call, as the Markdown report the GitHub Action posts: the docs references the pull request makes wrong ([below](#lint_docs-and-the-references-a-change-makes-wrong)), the examples run before and after, every impacted section with its evidence path, the cleared symbols, and verdicts recorded earlier. `run_examples: true` also runs up to 3 affected examples before and after (local servers only; takes minutes). | A Check node (graph mode) with its trace; on the first call, the pull request and its changes; with `run_examples`, Run nodes | | [`check_changes`](tools.md#check_changes) | Edits that are not a pull request yet: the doc sections they can make false with the evidence path for each, the examples [predicted to break](concepts.md#predicted-breaks), the impacted lines already out of date, and the changed symbols no doc reaches. Send `repo` and `files` (path and full new content, `null` to delete; at most 50), compared with the commit the graph indexed; a local server can instead read a checkout's working tree (`dir`, `base`). Also lists the docs references the edits make wrong (`newFindings`). | No (it may index the repository first) | | [`lint_docs`](tools.md#lint_docs) | Graph-free: every path, link, anchor, command, CLI flag, env var and docstring parameter a repository's docs, READMEs and AI agent files name that it does not have ([Lint](lint.md)). `repo` (and `ref`) lints a GitHub repository from its tarball, cached 10 minutes; a local server can lint a checkout (`dir`), with `new_only` for what its working tree introduced. `rules` and `files` narrow it. | No (it uses no graph and indexes nothing) | | [`docs_for_symbol`](tools.md#docs_for_symbol) | The doc sections and examples that depend on a symbol (an id, a qualified name, `Class.method` or a bare name), with the evidence path for each: what to update in the same change. | No | | [`audit_docs`](tools.md#audit_docs) | The [docs audit](audit.md) with no pull request: a 0-100 health score, every example line that no longer fits the code, API coverage, and the runs recorded at the indexed commits. It runs nothing. | No | | [`impact`](tools.md#impact) | The doc sections and examples a pull request can make false, with severity and the evidence path for each; 40 per call, page with `offset`. It also lists, over every page, the examples predicted to break and the impacted lines already out of date. | On the first call for a pull request: the pull request and its symbol-level changes. | | [`clearance`](tools.md#clearance) | The changed symbols that reach no doc within 3 call hops, each with a `git grep` command to check the claim. | As `impact`, if called first. | | [`read_section`](tools.md#read_section) | The current Markdown of a section and its code examples. | No | | [`run_example`](tools.md#run_example) | Runs an example in the sandbox, before and after a pull request with `pr_url`; identical runs are answered from the graph. Not on the hosted server. | Run nodes | | [`decisions_for`](tools.md#decisions_for) | Maintainer decisions governing a section or symbol, and earlier verdicts on a section. | No | | [`rank_sections`](tools.md#rank_sections) | Orders sections by the PageRank of their page in the docs' link graph. | No | | [`propose_edit`](tools.md#propose_edit) | Records replacement Markdown for a section (a `stale` verdict). Refused without a path from the pull request's changes to the section. | A verdict | | [`record_verdict`](tools.md#record_verdict) | Records `ok`, `governed` (must cite a decision) or `skipped` for one or more sections. | Verdicts | | [`write_decision`](tools.md#write_decision) | Records a maintainer decision and what it governs. Its description tells the model to call it only when a human asks. | A decision | | [`docs_patch`](tools.md#docs_patch) | The docs fix for a pull request as a unified diff, with a pull request title and description. MCP only. | No | ## lint_docs and the references a change makes wrong `lint_docs` answers without a graph, so it needs no indexing and works for any repository: ```json { "name": "lint_docs", "arguments": { "repo": "acme/app" } } ``` The summary is the lint summary line and the first five findings (`3 errors in 2 files (12 files checked, 840 ms).`, then `AGENTS.md:6 \`docs/setup.md\` does not exist; it moved to \`docs/guides/setup.md\`.`, …); the data is the [lint JSON](lint.md#json-output) (`version: 1`, `findings` with AI agent instruction files first). On a local stdio server, `{ "dir": ".", "new_only": true }` returns only what the working tree introduced against its base (`base`, default as for [`lint --new`](lint.md#only-what-a-change-introduced)). The hosted server reads public repositories only; a local server also sends `GITHUB_TOKEN` for private ones. `check_pull_request` and `check_changes` include the same kind of findings for a change: the references that were true before it and are not after it. `check_pull_request`'s report starts with a "Docs this pull request makes wrong" section ([the comment](github-action.md#docs-this-pull-request-makes-wrong)); `check_changes` returns `newFindings` (`{repo, base, head: "WORKTREE", findings, total, fixed, summary, files, ms}`) and adds `These changes make docs references wrong (…)` to its summary. ## Results A successful call returns three text blocks: 1. The summary, one or two sentences. 2. The data, as indented JSON. 3. The Cypher the tool ran, omitted when it ran no named query (`docs_patch`, for example): ```text Cypher run on FalkorDB: -- impact ( rows, ms) MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' … -- prChanges ( rows, ms) MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) … ``` The time is FalkorDB's own server-side execution time for that query. Small lookups the tools make with unnamed queries are left out. [Graph model and Cypher](GRAPH-MODEL.md) has every named query in full. `check_pull_request` is different: it returns two text blocks, the Markdown report (its format is in [GitHub Action](github-action.md#the-comment)) and `Stored as check . `. ## Errors Errors come back as a result with `isError: true` and one text block: | Cause | Message | |---|---| | Unknown tool | `unknown tool ` | | A parameter the tool does not declare | ` has no parameter ; its parameters are ` | | Invalid input | `invalid input: : ; …` | | The tool failed | The tool's message, for example `pr_url is required`, `refused: no path in the graph from 's changes to
. …`, ` has not been checked yet; call impact first`, ` is not indexed in the "" graph. Run: staleproof index …`, or `send the changed files: files: [{path, content}] with repo (this server cannot read your disk)` (`check_changes` with `dir` on an HTTP server) | | Hosted routing | `name a repository: …`, ` is not indexed yet: call check_pull_request with one of its pull requests first`, and the hosted limits ([Troubleshooting](troubleshooting.md#hosted-service)) | Unknown parameters are rejected with the same message the agent loop uses, so a client never gets a silently different answer. `propose_edit` and `record_verdict` need `pr_url` over MCP. In agent mode they default to the pull request under check; an MCP session has none, so without `pr_url` they fail with `pr_url is required`. Verdicts written over MCP belong to no check: they count for the patch and the pull request comment (the latest verdict per section wins), not for any agent check's stop condition. ## A worked session This is the sequence a client would call for [falkordb-py#267](https://github.com/FalkorDB/falkordb-py/pull/267), which fixed the client's execution-plan parser. The prompts are what you type; each block shows the `tools/call` parameters (`name` and `arguments`) the client sends. The client chooses the calls, so its order can differ. `check_pull_request` answers the first prompt in one call; this session takes the step-by-step route, which also yields the section and example ids. **"What docs can https://github.com/FalkorDB/falkordb-py/pull/267 make false? Show the evidence path for each."** ```json { "name": "impact", "arguments": { "pr_url": "https://github.com/FalkorDB/falkordb-py/pull/267" } } ``` The first call reads the pull request from GitHub and stores its changes. The graph reaches 3 doc sections, 2 of them in another repository: the GRAPH.EXPLAIN and GRAPH.PROFILE pages of `FalkorDB/docs`, through paths such as `ExecutionPlan._operation_tree ← ExecutionPlan.__init__ ← Graph.explain`. A grep for the changed names finds none of them. **"Read those sections and run their examples before and after the pull request."** ```json [ { "name": "read_section", "arguments": { "section_id": "FalkorDB/docs:commands/graph.explain.mdx#" } }, { "name": "run_example", "arguments": { "example_id": "FalkorDB/docs:commands/graph.explain.mdx:0", "pr_url": "https://github.com/FalkorDB/falkordb-py/pull/267" } }, { "name": "run_example", "arguments": { "example_id": "FalkorDB/docs:commands/graph.profile.mdx:0", "pr_url": "https://github.com/FalkorDB/falkordb-py/pull/267" } } ] ``` Take the section ids from the `impact` result. On the graph behind [Facts from the graph](FACTS.md), the GRAPH.PROFILE example passes before and after the pull request, and the GRAPH.EXPLAIN example fails both times with `Invalid graph operation on empty key`: it explains a query on a graph the page never creates. It was already broken, not by this pull request. When the graph already holds identical runs, the summary ends with `(recorded earlier; not re-run)`. On the hosted server `run_example` is not offered; ask the local server. **"Check for maintainer decisions, then record a verdict for each section."** ```json [ { "name": "decisions_for", "arguments": { "target_id": "FalkorDB/docs:commands/graph.profile.mdx#" } }, { "name": "record_verdict", "arguments": { "pr_url": "https://github.com/FalkorDB/falkordb-py/pull/267", "section_ids": ["
", "
"], "verdict": "ok", "reason": "The parser fix does not change what these sections claim; the example passes before and after.", "runs": ["run-", "run-"] } } ] ``` Run ids come from the `run_example` result (`runs[].id`). For the GRAPH.EXPLAIN page you can ask for a fix to its already-broken example with `propose_edit`. The tool accepts it because the graph has a path from the pull request to the section; its description asks the reason to say that the example fails before and after and to cite both runs. When `deepseek-v4-pro` re-checked #267 in agent mode (check `chk-90817863f077`, 12 tool calls), it considered that fix and declined, because the page assumes a demo graph that already exists, and recommended fixing it separately. **"Give me the docs patch for that pull request."** ```json { "name": "docs_patch", "arguments": { "pr_url": "https://github.com/FalkorDB/falkordb-py/pull/267" } } ``` With only `ok` verdicts, the summary is `No proposed edits for this pull request yet.` With proposed edits, the data holds the diff, a pull request title and description, and the changed files as `/:`; ask the client to apply the diff in your checkout of the docs repository. ### Before a pull request A coding agent changing the code can ask the same graph before anything is committed. These calls are from the test suite's fixture organisation, where `acme/mini-docs` documents `acme/mini-client`. **"I'm about to add a required `timeout` to `Graph.query`. Which docs depend on it?"** ```json { "name": "docs_for_symbol", "arguments": { "symbol": "Graph.query", "repos": ["acme/mini-client"] } } ``` The summary: `3 doc sections reach miniclient.graph.Graph.query (2 name or call it directly; 2 repositories). 2 line(s) in those examples already do not fit the code.` The data lists each section with its evidence path, for example `miniclient.graph.Graph.query ← miniclient.graph.Graph.ro_query ← acme/mini-docs:docs/getting-started.mdx:1`. **"I've made the change. Which examples does it break?"** ```json { "name": "check_changes", "arguments": { "repo": "acme/mini-client", "files": [{ "path": "miniclient/graph.py", "content": "" }] } } ``` The file is compared with the commit the graph indexed. The result's `impact.predicted` names the line that breaks: `docs/getting-started.mdx:0` line 18, `` `Graph.query` requires `timeout`, which the example does not pass ``. On a local stdio server, `{ "dir": "/path/to/checkout" }` checks the checkout's working tree instead of the files sent. Nothing is written to the graph. The rules to put in a repository's `AGENTS.md` or `CLAUDE.md`, so an agent does this unasked, are on [staleproof.dev/#/install](https://staleproof.dev/#/install) and in [Use cases](use-cases.md#coding-agents-before-the-pull-request). ## Security notes - **The local server runs code on your machine.** `run_example` (and `check_pull_request` with `run_examples`) clones the repositories, installs them from source, and executes docs code. The install step runs the projects' build steps with an allow-listed environment that leaves out `FALKORDB_URL`, `GITHUB_TOKEN` and model keys; the examples themselves run with a minimal environment and a socket-level block on network access other than the scratch FalkorDB. That is a guard, not a security boundary ([Example runner](example-runner.md#the-sandbox)). - **It flushes the scratch FalkorDB** before every example run. Staleproof refuses only when the scratch server is the one in `FALKORDB_URL`; make sure `STALEPROOF_SCRATCH_URL` (or the local port 6390) holds nothing you need. - **It writes to the graph.** Pull requests and their changes (`check_pull_request`, `impact`, `clearance`), checks (`check_pull_request`), runs (`run_example`), verdicts (`propose_edit`, `record_verdict`) and decisions (`write_decision`, which can also mark an older decision superseded). `check_changes`, `docs_for_symbol` and `audit_docs` write nothing beyond indexing a repository the graph lacks; `lint_docs` never touches the graph; the files sent to `check_changes` are not stored. Nothing is deleted, but whoever uses the server changes what later checks, the comment and the patch see. On the hosted server those writes go to a shared workspace graph that everyone checking that repository there sees. - **It writes nothing to GitHub.** No MCP tool opens a pull request or posts a comment; opening a docs pull request is the CLI's `patch --open-pr`. - **Credentials sit in the client configuration** in plain text, including any FalkorDB password in `FALKORDB_URL` and a `GITHUB_TOKEN`. The hosted server needs none. - Keep your client's approval prompts on for `run_example`, `propose_edit`, `record_verdict` and `write_decision`. ## Limits - `check_changes` checks edits statically: it predicts which examples break, but runs nothing. Running examples before and after needs a pull request (`run_example` with `pr_url`). The local server indexes the repositories it is asked about on first use (private ones with `GITHUB_TOKEN`); the hosted server reads public repositories only. - First-use indexing indexes the pull request's repository alone. For docs in another repository, index them together first (`staleproof index `), or use `staleproof check --with `, or name them next to the repository on the website. - The stdio server serves one graph per call: `STALEPROOF_ORG` when set, else the owner of the repository the call names. The hosted server picks the graph per repository. - No `--fresh` over MCP: an identical recorded run is always answered from the graph. Use `staleproof run --fresh` to execute it again. - Only `check_pull_request` stores a Check node; the other tools store no check and no trace. - `impact` returns 40 sections per call; for a larger pull request the client must page with `offset`, as the summary tells it to. - `/api/mcp` has no authentication, a rate limit of 300 requests per client per hour, and its calls do not wait in the HTTP server's one-check-at-a-time queue. ## Related - [Install](install.md) - [Tools reference](tools.md) - [Use cases](use-cases.md) - [Example runner](example-runner.md) - [Decisions and memory](memory.md) - [CLI](cli.md) --- # HTTP API `staleproof serve` runs one HTTP server with a JSON API, Server-Sent Events (SSE) streams for live checks, audits and indexing, a README badge, MCP over Streamable HTTP, and the web app. This page lists every route with its parameters, response fields, status codes, error bodies and rate limits, shows how to read the check stream from code, and covers hosted mode, where any public repository is indexed on demand. It is for developers who build a client, a dashboard or an integration against a running server. The same `/api` routes answer on a Vercel deployment such as staleproof.dev, through the function in [api/index.js](../api/index.js). ## Start the server and call it ```bash npx -y staleproof serve curl -s http://127.0.0.1:8080/api/health ``` The server binds `127.0.0.1:8080` by default (`--host`, `--port`, or `HOST` and `PORT`; see [Configuration](configuration.md#server)). On startup it prints one line to stderr, either `staleproof serve: http://127.0.0.1:8080 (graph "falkordb", agent on, ; MCP at /api/mcp)` or `... agent off: no model credentials, graph mode only; MCP at /api/mcp)`; in hosted mode `, hosted: any public repository on demand` follows the graph name. It connects to FalkorDB before it listens. With neither `FALKORDB_URL` nor `--db` set and nothing on `localhost:6379`, it starts a local FalkorDB first; with either set, an unreachable server stops it with `cannot reach FalkorDB at ...`. Run a graph-mode check and watch the trace arrive: ```bash curl -N -X POST http://127.0.0.1:8080/api/check \ -H 'content-type: application/json' \ -d '{"pr_url": "https://github.com/FalkorDB/falkordb-py/pull/262", "mode": "graph"}' ``` The API has no authentication. Anyone who can reach the port can start checks (which spend your model budget in agent mode), record decisions and review verdicts, and, in hosted mode, index repositories. Keep the default `127.0.0.1` bind, or put an authenticating proxy in front of it. ## Hosted mode Without hosted mode, the server answers from one graph: `STALEPROOF_ORG`, else `falkordb`. In hosted mode (`serve --hosted` or `STALEPROOF_HOSTED=on`; on by default in the Vercel function), that graph is the curated one, and any other public repository is downloaded as a tarball and indexed on demand into the workspace graph of its owner, `gh-`, together with the repositories named alongside it. The routes then find the right graph themselves: by the repository a pull request URL or an id names, and by searching every graph for a check or verdict id. [Deploy](DEPLOY.md#hosted-mode) has the limits and how workspaces are evicted. ## Routes at a glance | Method and path | Purpose | Rate limited | |---|---|---| | `GET /api/health` | Version, graph, FalkorDB version, whether agent mode is available, whether a check is running, hosted mode | no | | `GET /api/stats` | Every number FACTS.md quotes, computed from the graph | no | | `GET /api/prs` | The preloaded pull requests with their latest check and example runs | no | | `GET /api/checks` | Recent checks, newest first | no | | `GET /api/checks/:id` | The stored trace of one check | no | | `POST /api/check` | Run a check and stream its trace (SSE); hosted, index the repository first | yes, `check` bucket | | `POST /api/index` | Hosted mode: index repositories into one workspace and stream the progress (SSE) | yes, `index` bucket | | `GET /api/repos` | Which graph holds a repository, and whether it is indexed | no | | `GET /api/workspaces` | Hosted mode: every workspace and its repositories | no | | `GET /api/audit` | The docs audit of an indexed repository, as JSON | no | | `GET /api/lint` | Graph-free lint of any public repository (its tarball): the stale paths, links, commands and env vars its docs and AI agent files name | yes, `lint` bucket, cache misses only | | `POST /api/audit` | Audit a repository and stream the progress (SSE); hosted, index it first | yes, `audit` bucket | | `GET /api/badge/:owner/:name.svg` | The README badge: the repository's docs health score | no | | `GET /api/verified/:owner/:name.json` | The checked stamp's data: per-page status from the cached audit | no | | `GET /api/verified/:owner/:name/stamp.js` | The embeddable stamp script | no | | `GET /api/docs-for` | Which docs depend on a symbol, with the evidence path for each | no | | `POST /api/mcp` | MCP over Streamable HTTP | yes, `mcp` bucket (10 times the others) | | `GET /api/sections` | The text and examples of one doc section | no | | `GET /api/patch` | The docs patch for a checked pull request | no | | `POST /api/decisions` | Record a maintainer decision | yes, `write` bucket | | `POST /api/verdicts/:id/review` | Accept or reject a verdict | yes, `write` bucket | | `GET /api/model` | Every named Cypher query and every tool | no | | `POST /api/github/webhook` | The GitHub App's webhook: signed deliveries in, `202` at once, the work in the background (404 while the App is not configured) | no (signature checked) | | `GET /api/github/cron` | The App's daily job: docs fix pull requests, audit issues, dirty indexes (`Authorization: Bearer $CRON_SECRET`) | no | | `GET /`, `/llms.txt`, `/llms-full.txt`, `/docs/.md`, `/robots.txt`, `/sitemap.xml`, `/favicon.svg`, `/assets/*`, anything else | The web app and its static files | no | ## Conventions - Responses are JSON. Errors have the body `{"error": ""}` and a 4xx or 5xx status. An error a route does not handle itself becomes `400 {"error": ""}` when it is a hosted limit, a repository too large to index from an archive or a repository that is not indexed, and `500 {"error": ""}` otherwise. - Request bodies are JSON. Send `content-type: application/json`. A body that is not valid JSON is read as `{}`, so it fails validation with the message for a missing field. - Pull request URLs may be `https://github.com///pull/` (a trailing `/files`, query or fragment is allowed) or `/#`. They are normalised to `https://github.com///pull/`, which is how the graph stores them. - Ids follow the formats in [Core concepts](concepts.md#ids): a section is `owner/repo:path#anchor` (for example `FalkorDB/falkordb-py:README.md#asynchronous-example`), an example is `owner/repo:path:index`, a check is `chk-<12 hex>`, a verdict `ver-<12 hex>`, a decision `dec-<10 hex>`. URL-encode ids in query strings: they contain `/`, `:` and `#`. - Timestamps (`at`, `startedAt`, `finishedAt`, `createdAt`) are milliseconds since the Unix epoch. - No CORS headers are set, so a browser page on another origin cannot call the API directly. Call it from a server, or proxy it under your own origin. - Repository names may be `owner/name` or a GitHub URL (`https://github.com/owner/name`, with or without `.git`). They are case-insensitive, as on GitHub: a pull request URL in any spelling is read with the repository's indexed spelling, and hosted workspaces store names in lower case. ## Rate limits `POST` routes are limited to 30 requests per client per hour, and `POST /api/mcp` to 300 (a client sends a few requests per question). GET routes are not limited, except `GET /api/lint` when it has to download a repository (60 per hour). | Bucket | Routes | Limit | |---|---|---| | `check:` | `POST /api/check` | 30 per sliding hour | | `index:` | `POST /api/index` | 30 per sliding hour | | `audit:` | `POST /api/audit` | 30 per sliding hour | | `mcp:` | `POST /api/mcp` | 300 per sliding hour; over it, a JSON-RPC error `rate limited: try again later` with status 429 | | `write:` | `POST /api/decisions` and `POST /api/verdicts/:id/review`, together | 30 per sliding hour | | `lint:` | `GET /api/lint` answers that are not cached (and every `fresh=1`) | 60 per sliding hour (twice the others) | - **The client key** is the `X-Real-IP` header if present (Vercel sets it), else `Fly-Client-IP`, else the last entry of `X-Forwarded-For` (the one the nearest proxy appended), else the constant `local`. Behind no proxy, every caller shares the `local` bucket. - **Counting**: a request counts before it is validated, so a request rejected with 400 still uses one slot. - **Over the limit**: `429 {"error": "rate limited: try again later"}`. No `Retry-After` header is sent; the oldest request in the window drops out an hour after it was made. - **Scope**: counts live in memory, per process. A restart resets them; on Vercel each function instance counts on its own. - **Changing it**: `createApp({ rateLimit })` in the [library](library.md#mount-the-http-api-in-your-own-server). `staleproof serve` and the Vercel function use the default of 30; there is no flag or environment variable. - **Hosted agent checks** also have a daily budget across the service; see [POST /api/check](#post-apicheck). ## One check at a time A server runs one check at a time, agent or graph mode, and in hosted mode one indexing run at a time with them. Further `POST /api/check`, `POST /api/index` and (hosted) `POST /api/audit` requests wait in arrival order; a request that has to wait first receives a `queued` event. `GET /api/health` reports `busy: true` while one runs. Example runs are also serialised, process-wide, because they share the scratch database. Calls to `POST /api/mcp` do not wait in this queue. Closing the connection does not cancel a check. It runs to the end and is stored on its `Check` node; read it later with `GET /api/checks/:id`. ## GET /api/health What the server is connected to and whether agent mode is available. ```bash curl -s http://127.0.0.1:8080/api/health ``` A server without model credentials and without hosted mode answers: ```json {"ok": true, "version": "0.3.0", "org": "falkordb", "falkordb": "4.22.0", "agent": {"available": false, "reason": "the server has no model API credentials (ANTHROPIC_API_KEY)"}, "busy": false, "hosted": {"enabled": false}} ``` | Field | Type | Meaning | |---|---|---| | `ok` | `true` | Always `true` when the route answers | | `version` | string | The `staleproof` package version | | `org` | string | The graph name (the org slug) | | `falkordb` | string | The FalkorDB module version from `MODULE LIST`, or `unknown` if the server does not report it | | `agent` | object | `{available: true, model, effort, provider}` or `{available: false, reason}` | | `agent.provider` | string | The host of `ANTHROPIC_BASE_URL` for an Anthropic-compatible provider (for example `api.deepseek.com`), else `api.anthropic.com` | | `busy` | boolean | A check (or, hosted, an indexing run) is running | | `hosted` | object | `{enabled: false}`, or in hosted mode `{enabled: true, limits: {maxRepos, maxFiles, maxGraphMb}, memory: {used, max}, githubToken}`: the workspace limits, the FalkorDB server's memory in MB (`max` is 0 when uncapped; `memory` is left out when the server does not report it), and whether the server has a GitHub token | | `githubApp` | object | The [GitHub App](github-app.md): `{enabled: false}`, or `{enabled: true, appSlug, installUrl}` when the server has the App's credentials (`appSlug` and `installUrl` only when `GITHUB_APP_SLUG` is set; the web install page shows its button from it). Never a secret | `available: true` means credentials were found, not that they work. A `claude-*` model name with a compatible provider still reports `available: true`, and every agent check then fails with an `error` event (see [Troubleshooting](troubleshooting.md#common-errors)). ## GET /api/stats The facts behind [FACTS.md](FACTS.md), as JSON: the same object `staleproof facts --json` prints. It is computed from the graph and cached for 60 seconds per process. ```bash curl -s http://127.0.0.1:8080/api/stats ``` | Field | Shape | |---|---| | `org`, `generatedAt`, `falkordb` | graph name, ISO time, FalkorDB version | | `nodes` | `{` with the Cypher queries it ran and the time it took), any reminder, and a final line with the status, turns, tool calls, tool order, verdicts and tokens, followed by the model's summary. It ends with `Patch: staleproof patch Trace: stored on `. The exit code is 1 when the check ends `error` or `refused`. | Option | Default | Meaning | |---|---|---| | `--model ` | `STALEPROOF_MODEL`, else `claude-sonnet-5-5` | The model | | `--effort ` | `STALEPROOF_EFFORT`, else `medium` | `low`, `medium`, `high`, `xhigh` or `max` | | `--max-steps ` | `40` | Model turns before the loop stops | | `--json` | off | Print the whole result (events included) as JSON instead of the trace | | `--format markdown` | `text` | Also render the PR comment, with the agent's summary | | `--with ` | none | Repositories to index together with the pull request's when the graph lacks any of them | | `--no-index` | index on first use | Fail instead of indexing a repository the graph does not have | | `--out ` | none | Write the PR comment to a file | | `--no-agent` | off | Run [graph mode](#graph-mode) instead | To run on DeepSeek instead of Claude, see [The live demo's DeepSeek setup](#the-live-demos-deepseek-setup). The same loop runs behind `POST /api/check` in the [HTTP API](http-api.md) and the web UI. ## The loop The model chooses every tool call. The loop validates and executes the calls, streams trace events, enforces the stop condition and stores everything on a `Check` node. ### Setup 1. Ingest the pull request if it was never ingested ([Pull request ingestion](concepts.md#pull-request-ingestion)). 2. Find the most recent finished check in the graph and summarise what was written since it started (see [Since your last check](memory.md#since-your-last-check)). 3. Create the `Check` node: `status: 'running'`, the model name, `started_at`, linked `FOR` the pull request. The id is `chk-` plus 12 hex characters. 4. Send the system prompt, the tool definitions and one user message: `Check .`, followed by `Since the last check: …` when there is something to report. ### Each turn A step is one model turn, numbered from 1. - **Time budget**: if the check has a budget and it is used up, no new turn starts (see [Time budget](#time-budget)). - **The turn streams**: text and reasoning arrive as `delta` events (shown live, not stored); the complete message becomes `thinking` and `text` events. - **Retries**: an error that is not an API error, such as an unparseable streamed tool input, re-issues the turn, up to 2 times in a row; each retry uses a step. An API error ends the check as `error`. - **Stop reasons**: `refusal` ends the check as `refused` with the summary `The model declined to continue ().`; `pause_turn` continues with the next turn. A tool call cut off by `max_tokens` ends the check as `error`: `a tool call was cut off at max_tokens; raise max_tokens and re-run the check`. - **History** is append-only: the assistant message goes back to the model exactly as it came. - **Tool calls** in one message run one after another, in the order given. ### Tools offered The model gets the eight agent tools, with their JSON schemas: `impact`, `clearance`, `read_section`, `run_example`, `decisions_for`, `rank_sections`, `propose_edit` and `record_verdict`. `write_decision` is never offered: decisions come from people. `docs_patch` is for MCP clients. Each tool and its result are described in the [Tools reference](tools.md). For `impact`, `clearance`, `run_example`, `propose_edit` and `record_verdict`, a missing `pr_url` defaults to the pull request under check. ### Input validation Every tool call is validated before it runs, and every problem goes back to the model as an error result rather than ending the check: | Problem | Error result | |---|---| | Unknown tool | `unknown tool ` | | A parameter the tool does not have | ` has no parameter ; its parameters are `, for example `impact has no parameter limit; its parameters are pr_url, max_hops, offset` | | Invalid input | `invalid input for : : ; …` | | The tool throws | Its message, for example `refused: no path in the graph from 's changes to
. …` | Unknown parameters are an error, not ignored: if they were dropped silently, the model would get the same answer again and keep trying (see [the first #276 run](#the-first-276-run)). ### Tool results A successful result goes back to the model as JSON with the tool's `summary` and `data`, cut at 24,000 characters (`MAX_TOOL_CONTENT`) with `… [truncated; ask for fewer items]` appended. The trace keeps more: the full data (unless its JSON exceeds 60,000 characters, when it is stored as a preview marked `__trimmed`), plus every Cypher query the tool ran with its server-side time and row count. ### Stop condition The check is meant to end with a verdict or an edit for every impacted section. - **Impacted sections**: after any successful `impact` call, the loop collects the section ids of every page of that walk itself (fetching the pages the model has not asked for, without showing them to the model), so the set covers all impacted sections, not only the page the model saw. For #276 that is all 43. The set is the union over every `impact` call of the check, each with the `max_hops` it was called with. Example scripts, which have no section, are not included. - **When the model finishes** (a turn with no tool calls), the loop compares the set with the sections that got a verdict from this check. Its last text becomes the summary. - **Reminders**: if sections are still open, the loop sends up to 2 reminders: `These impacted sections still have no verdict or edit: . Give each one a verdict (record_verdict) or an edit (propose_edit), then finish.` - **Result**: `done` when nothing is open; `incomplete`, with the open sections listed in `missing`, when something still is after the reminders. A check in which the model never calls `impact` has no impacted sections to cover. ### Step limit The loop stops after `maxSteps` model turns: 40 by default, `--max-steps` on the CLI. The HTTP server always uses 40. A check that hits the limit ends `incomplete` with the summary `Stopped after 40 model turns with impacted section(s) still open.`, unless the model had already written a summary. ### Time budget A serverless function has a hard time limit, so the HTTP server can give each agent check a budget: `STALEPROOF_CHECK_BUDGET_MS`, which defaults to 240,000 (240 s) when `VERCEL` is set and is unset elsewhere. The budget is checked before each turn; a turn in progress finishes. Past it, the check ends `incomplete`: ```text Stopped at this server's 240 s time limit after model turns with impacted section(s) still open; the CLI has no time limit. ``` The CLI never sets a budget. A 43-section check such as #276 takes about 6 minutes, so on Vercel it ends incomplete; run it from the CLI. ### Statuses | Status | When | |---|---| | `done` | The model finished and every impacted section has a verdict or an edit | | `incomplete` | Sections still open after the reminders, the step limit or the time budget | | `refused` | The model returned a refusal | | `error` | An API error, a tool call cut off at `max_tokens`, a missing credential, or any other failure: `The check failed: ` | While it runs, the `Check` node has status `running`. A process killed mid-check leaves it there. ### What the Check node stores | Property | Meaning | |---|---| | `pr`, `model`, `started_at`, `finished_at` | What was checked, by which model, when | | `status`, `summary` | The outcome and the model's final summary (or the loop's) | | `steps` | Model turns used | | `tool_calls`, `tool_order` | How many tool calls, and their names in order | | `missing` | Impacted sections still without a verdict or edit | | `input_tokens`, `output_tokens` | Token usage, summed over turns | | `verdicts` | Verdicts written by this check | | `trace` | Every stored event as JSON | Edges: `(Check)-[:FOR]->(PullRequest)` and `(Check)-[:PRODUCED]->(Verdict)` for every verdict whose `check` is this id. The stored trace has these event types: `check_started` (with the "since" text), `thinking`, `text`, `tool_call` (name, input, step), `tool_result` (summary, `isError`, queries, data, milliseconds, step), `nudge` (the open sections) and `check_finished` (status, summary, steps, tool calls, tool order, verdicts, missing, usage). Read a stored trace with `GET /api/checks/`, or open `#/check/` in the web UI. ### Usage accounting Each turn's `input_tokens`, `output_tokens` and `cache_read_input_tokens` are summed. Input and output totals are stored on the `Check`; the cache-read total appears only in the trace's `check_finished` event. The CLI's final line shows input plus output tokens. Graph mode uses no tokens. ## The system prompt The system prompt ([src/agent/prompt.ts](../src/agent/prompt.ts)) states the job and the rules the graph enforces. It does not fix a tool order: the model decides what to look at next from what the tools return, so different pull requests produce different traces. Its rules, in short: - A path is a reason to look, not proof: "A path from a changed symbol to the section is the reason to look at it, not proof that it is wrong." - Breaking changes usually make a section wrong; behaviour changes often do not. - An example `impact` lists as predicted to break (its call no longer fits the changed code) is strong evidence the section is wrong: run it to prove it when possible, and fix the call to the new signature. Its "already did not fit" list is drift that predates the pull request. - Running a Python example before and after is the strongest evidence: passes before and fails after proves a break, passes both is good evidence the docs hold, fails both means it was already broken, and skipped runs prove nothing. - Check `decisions_for` before editing, and cite a governing decision with a `governed` verdict. Use earlier verdicts, and whether maintainers accepted them. - Never propose an edit without a path from the pull request's changes; `propose_edit` refuses otherwise. - What to edit: "Edit what is wrong: what the pull request falsified, or an example you ran that fails both before and after (already broken: say so in the reason, cite both runs, and fix only what makes it fail)." Keep the author's voice and everything still true, and give the whole section, heading included, as `new_text`. - Every impacted section must end with a verdict or an edit; one reason may cover many sections. - Do not invent APIs. - Finish with a short plain-text summary for a maintainer: what changed, what was edited and why, what ran and what it proved, and what could not be checked. The path guard in `propose_edit` is the same for an already-broken example: the section must still be reachable from the pull request's changes. ## Model providers The agent was written for Claude and runs on any API that speaks Anthropic's Messages format. Both go through `AnthropicModel` ([src/agent/anthropic.ts](../src/agent/anthropic.ts)). | Option | Default | Meaning | |---|---|---| | `model` | required | The model name (`STALEPROOF_MODEL` or `--model` on the CLI) | | `effort` | `medium` | `low`, `medium`, `high`, `xhigh` or `max` | | `fallbacks` | `true` | Server-side refusal fallback (Claude API only); `STALEPROOF_FALLBACKS=off` disables it | | `apiKey` | none | Otherwise the SDK finds credentials itself | | `maxTokens` | `32000` | Output limit per turn; set only through the library | | `baseURL` | none | The API endpoint (`ANTHROPIC_BASE_URL`) | | `compatible` | `false` | Send only the portable request fields; set when `ANTHROPIC_BASE_URL` names a host other than `api.anthropic.com` | ### Claude Claude is the default (`claude-sonnet-5-5`). Each turn is a streamed request with: - adaptive thinking with summarised display, so the trace shows why each tool was chosen; - the effort level set explicitly; - the system prompt marked for prompt caching; - tool inputs streamed as they are generated (the loop validates every input before running it); - the server-side refusal fallback, unless `STALEPROOF_FALLBACKS=off`. The Claude path is tested against a local fake API only: [test/anthropic-model.test.ts](../test/anthropic-model.test.ts) checks the request it sends and that it reads a streamed answer, without paid calls. The agent runs with a real model have used DeepSeek. ### Anthropic-compatible providers Set `ANTHROPIC_BASE_URL` to a provider's Anthropic-compatible endpoint. Any host other than `api.anthropic.com` (or a URL that does not parse) switches to compatible mode, which sends only the portable fields: model, max tokens, the system prompt as plain text, the tools, the messages, thinking enabled with a budget of 16,000 tokens, and the effort level. No prompt caching, beta headers, refusal fallback or streamed tool inputs. A Claude model name is refused in compatible mode, because the provider would answer with its own model under that name and every trace would record the wrong model: ```text "claude-sonnet-5-5" is a Claude model name, but the model endpoint is an Anthropic-compatible provider, which would answer with its own model under that name; set STALEPROOF_MODEL to the provider's model (e.g. deepseek-v4-pro) ``` The default model is `claude-sonnet-5-5`, so setting `ANTHROPIC_BASE_URL` without `STALEPROOF_MODEL` hits this error. ### The live demo's DeepSeek setup The live demo runs `deepseek-v4-pro` through DeepSeek's Anthropic-compatible endpoint: ```bash export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic export ANTHROPIC_API_KEY= export STALEPROOF_MODEL=deepseek-v4-pro npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/267 ``` For a server, set the same three variables in its environment (see [Deploy](DEPLOY.md)). `GET /api/health` then reports the agent as available, with the model, the effort and the provider's host (`api.deepseek.com`). ### Credentials The agent needs credentials: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, a selected `ANTHROPIC_PROFILE`, a federation rule with an identity token, or an `ant auth login` profile on disk. Without any, agent mode fails before the check starts, instead of falling back to graph mode: ```text the agent needs model API credentials: set ANTHROPIC_API_KEY (or run `ant auth login`) for the Claude API, or ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY and STALEPROOF_MODEL for an Anthropic-compatible provider such as DeepSeek. Use `check --no-agent` to run the graph tools without a model. ``` An authentication failure during a turn is reported the same way, with the provider's message appended. The HTTP server without credentials offers graph mode only: `POST /api/check` with `mode: 'agent'` answers 409. ## Graph mode Graph mode runs the same tools in a fixed order with no model. It needs no credentials, and it is labelled as graph mode everywhere it is shown (the `Check`'s model is `graph-only (no model)`). ```bash npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/267 --no-agent ``` The order: 1. `impact`, with `max_hops` when it is not 3. 2. `clearance` (always 3 hops). 3. `rank_sections` over the impacted section ids of the first page, if there are any. 4. `run_example` with the pull request, for up to `maxRuns` distinct examples: first those `impact` predicts to break (with an `exact` match), so a run can prove the prediction, then examples from the `exact` sections of the first page. Graph mode samples: it ranks the first `impact` page (40 sections) and runs at most 3 examples by default, so on a pull request with more sections, the rest are listed in the impact count but not ranked or run. It writes no verdicts and no edits, does not read decisions, sends no reminders and computes no "since" summary. A tool error is recorded in the trace and the remaining steps continue; only a failing `impact` ends the check as `error` (`The check failed: impact failed`). On #262 it makes 4 calls: impact, clearance, rank_sections and one run_example. | Setting | CLI | HTTP server | MCP `check_pull_request` | |---|---|---|---| | Example runs (`maxRuns`) | `--runs `, default 3; `--runs 0` runs none | 3; none in a hosted workspace | 0; 3 with `run_examples: true` | | Call hops (`maxHops`) | `--max-hops `, default 3 | Always 3 | Always 3 | The summary is the impact and clearance summaries, then `Ran example(s) before and after the PR: : ; …` (or `Example runs not requested.` or `No exactly-linked Python example to run.`), then `Graph mode: fixed tool order and no verdicts; agent mode (a model) judges each section and proposes the edits.` The CLI prints the impacted sections, the cleared symbols, each run and the summary, and stores the check like an agent check. `check --no-agent --format markdown`, which the GitHub Action runs without a model key, runs this same graph-mode check and then renders the report from the graph: the examples run before and after, impact, clearance, ranking and any verdicts already recorded. The MCP tool `check_pull_request` does the same and returns the report. ## ScriptedModel for tests `ScriptedModel` is a deterministic stand-in for a model, exported by the library. You give it a function from the conversation so far and the turn number to the turn's content: `thinking`, tool `calls`, or final `text`. A turn with calls ends with `tool_use`, one without with `end_turn`; usage is zero. It records every request it receives in `requests`, so a test can check what the model was shown. ```ts import { GraphStore, loadConfig, runAgentCheck, ScriptedModel } from 'staleproof'; const config = loadConfig(); const store = await GraphStore.connect(config.falkordbUrl); // The check is stored like any other: use a graph where the pull request's repository is indexed. const graph = store.org(config.org); // Turn 0 calls impact; every later turn finishes without a verdict. const model = new ScriptedModel((history, turn) => turn === 0 ? { calls: [{ name: 'impact', input: {} }] } : { text: 'Done.' }, ); const result = await runAgentCheck({ graph, config }, model, 'https://github.com/FalkorDB/falkordb-py/pull/262', { maxSteps: 5 }); console.log(result.status, result.missing); // incomplete, after two reminders, with the open section listed await store.close(); ``` The test suite drives the loop this way against an indexed fixture organisation ([test/agent.test.ts](../test/agent.test.ts)): a removed method that a script reads and edits, a signature change the script forgets to close (the loop must remind it), tool errors, the time budget, unknown parameters, and the stop condition across impact pages. ## Traces from FalkorDB's pull requests These checks ran `deepseek-v4-pro` on FalkorDB's graph. Each pull request got a different tool order. **falkordb-py#262**: 5 tool calls, 1 verdict. | Step | Tool | Result summary | |---|---|---| | 1 | `impact` | `1 changed symbols reach 1 doc sections (0 breaking, 0 signature, 1 behavior; 0 in other repositories).` | | 2 | `read_section` | `falkordb-py > Usage > Asynchronous Example (README.md, lines 64-114, 1 examples).` | | 3 | `decisions_for` | `No decision governs this.`, followed by the section's earlier verdicts | | 4 | `run_example` | `Fails at 49aa012 and passes at f7c3de7: the PR fixes this example. (recorded earlier; not re-run)` | | 5 | `record_verdict` | `Verdict ok recorded for 1 section.` | The impact summary is shown as recorded then; the current wording is `1 changed symbol reaches 1 doc section (…)`. The model concluded that the pull request fixes the documented example rather than breaking it, so the section needs no edit. **falkordb-py#267**: 15 tool calls and 3 verdicts, each citing its before/after runs. A later check under the current prompt (`chk-90817863f077`, 12 tool calls: impact, read_section and decisions_for for each of the 3 sections, 3 run_example calls, then 2 record_verdict calls) again closed all 3 sections as `ok`. It considered the `GRAPH.EXPLAIN` example, which fails before and after, and declined to edit it: the page assumes a demo graph that already exists, so the model judged that fixing it was outside this pull request's scope and recommended a separate docs fix in its summary. **falkordb-py#276**: 38 tool calls paging through all 43 impacted sections with `offset`, every one with a verdict. No check on FalkorDB's graph has proposed an edit: every verdict on #262, #267 and #276 is `ok`. These pull requests are fixes, and no example they reach broke because of them. Proposing edits and building the docs patch are exercised on the test suite's fixture organisation. ### The first #276 run The first agent check of #276 stopped at the 40-turn limit with 21 sections open. At the time: - `impact` returned at most 40 sections and had no way to page, while #276 reaches 43. - The model tried parameters the tool did not have, such as `limit`, `max_results` and an `offset` that did not exist yet. They were dropped silently, so it got the same first page back each time. - The stop condition counted only the sections the model had been shown, so the 3 sections past the first page were never required. What changed: `impact` pages (40 sections per call, `offset`, and a summary that says `call impact again with offset=40 for the rest`); unknown parameters are an error in the agent loop and over MCP; and the loop collects every impact page itself, so `done` means all impacted sections have a verdict or an edit. The next check of #276 paged through all 43 sections in 38 calls. ## Limits - **The Claude path is tested against a local fake API only.** Real agent runs so far used `deepseek-v4-pro`. - **Model choices vary between runs**: the same pull request can get a different tool order and different verdicts. - **The time budget applies to the HTTP server only**; on Vercel a large check ends incomplete at 240 s. - **`max_tokens` is set only through the library**, though the cut-off error says to raise it. - **`--effort` on the CLI is passed through unchecked**; only `STALEPROOF_EFFORT` is validated. - **The HTTP server has no step, run or hop options**; it uses the defaults. ## Related - [Core concepts](concepts.md#agent-mode-and-graph-mode): agent mode and graph mode side by side - [Tools reference](tools.md): what each tool takes and returns - [Decisions and memory](memory.md): what checks read and write back - [Configuration](configuration.md): the model and budget environment variables - [Deploy](DEPLOY.md): running the agent on a server --- # Lint rules Generated by `npm run docs` from `src/lint/rules.ts`: every rule `staleproof lint` knows, its default severity, where it looks, and what a finding says. Change a severity or turn a rule off in `.staleproof.toml` (`[rules] "rule-id" = "off" | "info" | "warning" | "error"`); silence one line with ``. Beta rules report at info and never fail a check until `[rules]` opts them in ([False positives](false-positives.md#how-maturity-works)). No rule is free of false positives: the evidence under each rule says what was checked and what was wrong. See [Lint](lint.md). | Rule | Severity | Maturity | Checks | Fix | |---|---|---|---|---| | [`path-missing`](#path-missing) | info (error when opted in) | beta | agent files, READMEs, docs | yes | | [`line-out-of-range`](#line-out-of-range) | info (warning when opted in) | beta | agent files, READMEs, docs | no | | [`link-broken`](#link-broken) | error | stable | agent files, READMEs, docs | yes | | [`anchor-missing`](#anchor-missing) | info (warning when opted in) | beta | agent files, READMEs, docs | yes | | [`command-missing`](#command-missing) | error | stable | agent files, READMEs, docs | yes | | [`cli-command-unknown`](#cli-command-unknown) | error | stable | agent files, READMEs, docs | yes | | [`cli-flag-unknown`](#cli-flag-unknown) | info (error when opted in) | beta | agent files, READMEs, docs | yes | | [`env-unused`](#env-unused) | warning | stable | agent files, READMEs, docs, env files | yes | | [`env-undocumented`](#env-undocumented) | info | stable | env files | no | | [`symbol-missing`](#symbol-missing) | info (error when opted in) | beta | agent files, READMEs, docs | yes | | [`docstring-param-unknown`](#docstring-param-unknown) | error | stable | docstrings | yes | | [`docstring-param-undocumented`](#docstring-param-undocumented) | info | stable | docstrings | no | | [`example-unknown-keyword`](#example-unknown-keyword) | info (error when opted in) | beta | code examples | yes | | [`example-too-many-arguments`](#example-too-many-arguments) | info (error when opted in) | beta | code examples | no | | [`example-missing-argument`](#example-missing-argument) | info (error when opted in) | beta | code examples | no | | [`example-missing-import`](#example-missing-import) | info (error when opted in) | beta | code examples | yes | | [`example-missing-attribute`](#example-missing-attribute) | info (error when opted in) | beta | code examples | yes | | [`example-removed`](#example-removed) | error | stable | code examples | yes | ## path-missing A code span names a file or directory of the repository that does not exist. Readers and coding agents open the path, find nothing, and guess. Moved and deleted files are the most common drift in agent instructions. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "path-missing" = "error"` opts it in. Hand-checked on 8 repositories (the 0.4 precision runs: httpx, flask, TypeScript, python-sdk, codex, pydantic-ai, vercel/ai, this one): every final finding true (codex 3, pydantic-ai 5), 20 verified fixes on pydantic-ai; true again on pmndrs/valtio (2, a deleted Next.js route its README names). First-run false positives (other projects' paths in docs, skill-relative paths) closed by guards: a docs page needs git-history evidence. Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 7 true, 32 false; the 0.8.0 guards leave 7 true, 0 false: placeholder segments (`../NNNN/results.md`, `your_provider.md`), pages of history (changelogs, plans, proposals, decision records, issue drafts) and templates (`templates/AGENTS.md`), check, label and branch names (`license/cla`), paths the sentence says are gone, where another tool looks (`.github/skills/`), sibling folders outside the repository, module paths without their extension, paths relative to a package or skill the paragraph names or to the code an agent file governs (`../fixtures`, a Cursor rule's `globs`), a path like its siblings (`test_provider/evals`). Fixture `path-missing-zod-placeholder`. But the held-out check (Oct 18: 18 popular repositories in none of the hall or the earlier runs, linted only after those guards were written, every warning and error hand-checked) found 0 true, 12 false: mostly records of a removal without a PR reference, another repository's paths, and plans. Beta in 0.8.0 under the 80% rule. - Checks: agent files, READMEs, docs Reported on: ```text Config lives in `src/settings/defaults.ts`. ``` Message: `src/settings/defaults.ts` does not exist in the repository. ## line-out-of-range A `path:line` reference points past the end of the file. Line references rot as files shrink; the reader lands on the wrong code. - Default severity: **info** (**warning** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "line-out-of-range" = "warning"` opts it in. A line count on a file path-missing resolved in this repository, so it shares that rule's guards. No finding, true or false, on the 8 the 0.4 precision runs repositories or the 14 of the Oct 18 sweep (zustand, zod, jotai, typer, ky, click, starlette, rich, black, sqlmodel, execa, valtio, hono, vite). The held-out check (Oct 18: 18 fresh repositories, hand-checked) found 0 true, 1 false and 2 unclear, and no true finding on any of the 81 repositories checked: beta in 0.8.0 under the 80% rule. - Checks: agent files, READMEs, docs Reported on: ```text See `src/cli.ts:812`. ``` Message: `src/cli.ts` has 523 lines; line 812 does not exist. ## link-broken A relative link points to a file that does not exist. Broken links in docs and agent files send readers (and agents following them) nowhere. - Default severity: **error** - Maturity: **stable**. True findings on TypeScript (1), vercel/ai (25, 31 verified fixes), colinhacks/zod (3) and pmndrs/jotai (3, a renamed docs folder). First-run false positives (mkdocstrings-generated pages, translations, docs-root links) closed by guards. Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 586 true, 418 false (58%), almost all docs-site routing; the 0.8.0 guards leave 586 true, 9 false (98%), none of the true ones lost: MkDocs configs split over files (textual's raw-HTML hrefs) and TanStack's `docs/*/config.json` count as docs sites, pages served without their order prefix (svelte, gradio), `entry.server.tsx.md` pages, doc-builder `_redirects.yml` (the English one for translations), Storybook `staticDirs`, fragments a site includes (snippets, `docs_src/` annotations), Vue bindings (`:href`), placeholder targets, templates, sample data, prefixes a generated page says it omits. A host written without its scheme (textual's `github.com/textualize/rich`) is still reported on a docs site. Left: svelte's compiler messages (7), react-router 2. Fixtures `link-broken-vite-vue-bound-href`, `link-broken-sqlmodel-included-fragment`. - Checks: agent files, READMEs, docs Reported on: ```text Read the [deploy guide](docs/deploy.md). ``` Message: The link target `docs/deploy.md` does not exist. ## anchor-missing A link to `#anchor` matches no heading in the target page. Renamed headings silently break deep links: the page opens at the top. - Default severity: **info** (**warning** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "anchor-missing" = "warning"` opts it in. True findings on httpx (6) and zod (5), but more false ones on fresh repositories (Oct 18 sweep): pmndrs/zustand 8 of 8 (its docs site keeps punctuation in heading ids, and `<#a-(b)-c>` was cut at the first `)`; 0.7.0 reads the whole destination and reports a heading found under another slugger at info, with no fix), colinhacks/zod about 12 of 18 (headings after a code block that closes its HTML comment on the fence line, ``` -->, and links inside MDX comments: both fixed in 0.8.0). Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 148 true, 249 false (37%), most from docs-site sluggers (MkDocs attr_list and toc, VitePress, Mintlify, TanStack, hf doc-builder). Fixtures `anchor-missing-zustand-*`, `anchor-missing-zod-*`. After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack, above info (a heading under another slugger is an info note with no fix): 134 true, 23 false (85%), from 109 false: MkDocs attr_list ids (`{ #id }`, fastapi 35, marimo 12), hf doc-builder `[[id]]` ids and slugs (`HF_TOKEN` → `hftoken`, huggingface_hub 30), generated API anchors (`[[autodoc]]`, `:::`, smolagents 10). Still beta for 0.8.0 (decided before this measurement; crewAI's 207 anchors are not judged): a candidate for stable. - Checks: agent files, READMEs, docs Reported on: ```text See [options](cli.md#global-flags). ``` Message: `cli.md` has no heading `#global-flags`. ## command-missing A documented command (`npm run x`, `make x`, `just x`, `uv run x`, ...) is not defined by the repository. Setup and contributing steps are run verbatim, by people and by agents; a renamed script fails the first step. - Default severity: **error** - Maturity: **stable**. the 0.5 precision runs: 22 repositories, 1 300+ invocations judged, 5 findings, all true (codex 1, cline 4); 17 verified fixes on simulated renames; nothing false on the 14 repositories of the Oct 18 sweep. Every first-run false positive (go-task's users' tasks, another project's layout after `cd`) closed by a guard. Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 13 true, 7 false (65%); the 0.8.0 guards (`make format/lint/typecheck` shorthand for existing targets, a sentence that says the command is not there, blocks introduced as output) leave 13 true, 2 false (87%): nuxt's and express's scripts of the reader's generated app. - Checks: agent files, READMEs, docs Reported on: ```text Run `npm run lint:docs` before pushing. ``` Message: `lint:docs` is not a script in package.json. ## cli-command-unknown The repository's own CLI is shown with a subcommand it does not have. CLI docs drift with every rename; users copy the command and get a usage error. - Default severity: **error** - Maturity: **stable**. the 0.5 precision runs: the CLI models of 13 repositories (click, typer, argparse, commander) judged against 1 300+ invocations with no false positive left (none either on the Oct 18 sweep: typer, click, black, rich); first-run ones (log lines like `staleproof mcp:`, an unrelated parser reached through `main()`) closed by guards. Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 7 true, 6 false (54%), all crewAI's `uv tool list` output (`crewai v0.102.0`) in a shell block; 0.8.0 reads a block introduced as output, or holding listing rows, as output: 7 true, 0 false. - Checks: agent files, READMEs, docs Reported on: ```text `mytool sync --all` ``` Message: `mytool` has no command `sync` (commands: pull, push). ## cli-flag-unknown The repository's own CLI is shown with an option it does not accept. Removed and renamed flags are a classic source of copy-paste failures. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "cli-flag-unknown" = "error"` opts it in. the 0.5 precision runs: 22 repositories and 1 300+ invocations, leaf options judged (llm, datasette, sqlite-utils, hatch), no false positive left after the guards. But the Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift) found 6 true, 245 false (2%): commander subcommands defined in other modules and mounted with `addCommand` (shadcn-ui 110), a launcher that forwards argv to sub-CLIs (modelcontextprotocol/inspector 134), two CLIs with one name (mem0). Beta until the CLI model follows them (src/lint/facts-cli.ts). - Checks: agent files, READMEs, docs Reported on: ```text `mytool push --force-all` ``` Message: `mytool push` has no option `--force-all`. ## env-unused A documented environment variable is never read by the code. Users set it and nothing changes; usually the variable was renamed. - Default severity: **warning** - Maturity: **stable**. Every application in the 0.5 precision runs had first-run false positives, each closed by a per-ecosystem guard (pydantic-ai 11, vercel/ai 67, uvicorn's FORWARDED_ALLOW_IPS, Blaxel's BL_REGION). Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 67 true, 11 false (86%), so stable again (C had made it beta in 0.8 on one repository): pmndrs/zustand's `MDX` and `HOME_REDIRECT` in CONTRIBUTING.md, read by the pmndrs/docs checkout the step tells you to make, are guarded (a variable set in another repository's checkout is not judged), fixture `env-unused-zustand-other-checkout`; left: variables of another product the docs configure (LiteLLM's in adk-python, TAVILY_API_KEY in langgraph). - Checks: agent files, READMEs, docs, env files Reported on: ```text Set `DATABASE_URI` to your database. ``` Message: `DATABASE_URI` is documented but no code reads it. ## env-undocumented The code reads an environment variable the repository's env file does not list. New settings that never reach `.env.example` are found by reading the source. - Default severity: **info** - Maturity: **stable**. Already info. the 0.5 precision runs: every info on flask, python-sdk, codex, openai-agents-js, OpenHands and vercel/chatbot checked, samples on vercel/ai, browser-use and cline: all true; test-runner variables (PLAYWRIGHT) and test configs closed by guards. - Checks: env files Reported on: ```text process.env.REDIS_TLS ``` Message: `REDIS_TLS` is read in src/db.ts but not listed in .env.example. ## symbol-missing A code span names a member of a repository class or module that does not exist. Prose that names `Client.query_v2` after it was renamed teaches readers and agents an API that is gone. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "symbol-missing" = "error"` opts it in. No true finding on any real repository before the Drift Hall: all 36 of its first-run findings (python-sdk 28, pydantic-ai 7, flask 1) were false and became guards (a package name bound as a variable, members need a call, history per sentence). Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 7 true, 28 false (20%), the false ones in release notes and proposals (scrapy's `docs/news.rst`, `sep/`). - Checks: agent files, READMEs, docs Reported on: ```text Call `Client.query_v2()`. ``` Message: `Client` has no member `query_v2`. ## docstring-param-unknown A docstring documents a parameter the function's signature does not have. Docstrings feed IDE hovers, API reference sites and agents; a renamed parameter makes them wrong everywhere. - Default severity: **error** - Maturity: **stable**. 27 true errors, each checked: vercel/ai 2, OpenHands 1, cline 13, zod 1 (`ZodDiscriminatedUnion.create` `@param types`), Textualize/rich 9, vitejs/vite 1; none false on the Oct 18 sweep. First-run false positives (a `# noqa` comment hiding a parameter, `_x` for `x`, inline object fields, JSDoc in prose) closed by guards. Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift): 172 true, 18 false (91%): type-first entries (`int (size)`), section headers indented inside `Args:`, nameless JSDoc `@param {object} A server address`, sample docstrings in a code block. - Checks: docstrings Reported on: ```text Args: timeout_ms: ... ``` Message: `connect` has no parameter `timeout_ms` (parameters: host, port, timeout). ## docstring-param-undocumented A docstring documents most of the parameters but leaves one out. Partly documented signatures usually mean a parameter was added without its docs. - Default severity: **info** - Maturity: **stable**. Already info. Hand-checked on flask (4), python-sdk (13), codex, cline, openai-agents-js, pallets/click (11): all true once docstrings that document a minority of parameters on purpose were left out (at least half must be documented). - Checks: docstrings Reported on: ```text def connect(host, port, tls): """Args: host, port""" ``` Message: `connect` documents host, port but not `tls`. ## example-unknown-keyword A docs code example passes a keyword argument the function does not accept. The example fails when copied: a TypeError in Python, a type or compile error in TypeScript and Go. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "example-unknown-keyword" = "error"` opts it in. Static example checks since 0.3: every finding on the 8 original repositories real drift; the 0.5 language runs regression runs unchanged. But the Drift Hall (Oct 18: 63 repositories Staleproof was never tuned on, every warning and error hand-checked, staleproof.dev/#/drift) found 13 true, 876 false (1%): the first `@overload` taken without overload resolution made every `instructor.from_provider(...)` client the wrong class (567-labs/instructor 851), receivers typed wrong (`with X() as y`, `await`, containers). After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack: 9 true, 882 false before, 9 true, 30 false after (23%): an overloaded call's result is known only when every overload agrees, and a name on a docs page is its latest binding above the line. Left: history pages (fastmcp's `docs/v2`, mem0), receivers of `with` and `await`. - Checks: code examples Reported on: ```text Client(hostname="db.local") ``` Message: `Client` has no parameter `hostname`. ## example-too-many-arguments A docs code example passes more positional arguments than the function takes. The example fails when copied: a TypeError in Python, a type or compile error in TypeScript and Go. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "example-too-many-arguments" = "error"` opts it in. True: vercel/ai 4 (constructors that dropped `settings`). zod's 14 first-run false positives (API listings, versioned docs) closed by guards. Drift Hall: 11 true, 21 false (34%). After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack: 5 true, 21 false before, 3 true, 7 false after (30%): a function the page defines itself is the reader's code and is not judged (pmndrs/jotai's tutorial that rebuilds `atom`, fixture `example-too-many-arguments-jotai-tutorial`), which also drops 2 true findings (a snippet calling its own function wrongly: TanStack/router, react-router). - Checks: code examples Reported on: ```text g.query("MATCH (n) RETURN n", {}, 30) ``` Message: `Graph.query` takes at most 2 positional arguments; the example passes 3. ## example-missing-argument A docs code example leaves out a required argument. The example fails when copied: a TypeError in Python, a type or compile error in TypeScript and Go. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "example-missing-argument" = "error"` opts it in. True on flask and pmndrs/jotai (`useFocusAtom(anAtom)` without `keyFn`, a hook the page defines, no longer judged in 0.8.0); the zero-argument calls of API listings are guarded (zod). Drift Hall: 21 true, 856 false (2%). After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack: 9 true, 855 false before, 5 true, 37 false after (12%): overloads that disagree (instructor 456), arity from a declared function type (`export const f: F = (a, b) => …`, TanStack/router 315), functions the page defines (vitejs/vite's preset shadowed by an import, fixture `example-missing-argument-vite-shadowed-by-import`). - Checks: code examples Reported on: ```text UserList.as_view() ``` Message: `View.as_view` requires `name`. ## example-missing-import A docs code example imports a name the package does not export. The example fails at its first line. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "example-missing-import" = "error"` opts it in. True: pmndrs/jotai 4 (`atomFamily` and `loadable` left `jotai/utils` in v3), pmndrs/valtio 1 (a typo). But JavaScript and TypeScript exports are not modelled completely: Drift Hall 175 true, 1 818 false (9%): `exports` maps and subpaths, `export *` chains, CommonJS and `export =` typings, unbuilt `dist/` entries. After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack: 77 true, 1 660 false before, 77 true, 1 624 false after (5%): names bound by destructuring and exported by a list (execa 24), re-exports from files the checkout lacks, `.d.ts` subpaths (vite). Still open: types only a hand-written `index.d.ts` declares (declaration files are not indexed). Fixtures `example-missing-import-*`. - Checks: code examples Reported on: ```text from miniclient import connect ``` Message: `miniclient` has no `connect`. ## example-missing-attribute A docs code example calls an attribute the class or module does not have. The example raises an AttributeError when copied. - Default severity: **info** (**error** when opted in) - Maturity: **beta**: reported at **info**, lower confidence, until `[rules] "example-missing-attribute" = "error"` opts it in. Drift Hall: 24 true, 676 false (3%), mostly an exported instance read as the module of the same name (trpc's `initTRPC`, TanStack/query's managers). After the 0.8.0 guards, on the 61 hall repositories other than crewAI and haystack: 24 true, 679 false before, 24 true, 594 false after (4%): an alternative tab with no import of its own (zod's "Zod Mini" blocks on api.mdx, 18) is not judged against the default tab's import, fixture `example-missing-attribute-zod-mini-tabs`; vitejs/vite 2 (`ModuleRunner.import`, a method the class defines) still open. - Checks: code examples Reported on: ```text client.create_constraint(...) ``` Message: `FalkorDB` has no attribute `create_constraint`. ## example-removed A docs code example calls a symbol that was removed. The example fails when copied. - Default severity: **error** - Maturity: **stable**. Reported only by pull request checks, from the graph: an example calls a symbol the pull request removes or renames, read from the diff itself. No false positive reported yet. - Checks: code examples Reported on: ```text legacy_connect() ``` Message: `legacy_connect` was removed. --- # False positives A wrong finding is a bug in Staleproof, never in your docs. Every finding can be reported in one click; each report becomes a regression fixture that every release runs, so the same false positive cannot come back; and a rule that has not earned trust yet reports at info, where it can never fail a check or lower a score. ## The promise - **Report it in one click.** Wherever findings are listed, a "False positive?" link opens a GitHub issue on [rajkaria/staleproof](https://github.com/rajkaria/staleproof/issues/new?template=false-positive.yml) with the rule, the repository and commit, the file and line, the message, the flagged line and the Staleproof version already filled in. You add one sentence: why it is not wrong. - **It never comes back.** The report becomes a fixture in `test/fixtures/fp/`: the page and only the files the finding depends on, and an `expected.json` that says "no finding `` at `:`". The test suite lints every fixture, and a release cannot ship while one of those findings comes back. - **Trust is earned.** A rule is `stable` only when its findings have been hand-checked on real repositories it was never tuned on and they were mostly right. Stable rules still make mistakes: each one reported becomes a fixture. Until a rule earns it, it is `beta`: still shown, in its own group, but it cannot fail your CI or lower your docs score. ## Report a wrong finding | Where | What you get | |---|---| | `staleproof lint` (text) | One line at the end of a report with findings: the form's address. Never a link per finding, and nothing at all when there is nothing to report. | | `staleproof lint --format markdown`, the pull request comment, the GitHub App's check summary | `False positive?` after each finding, prefilled. | | `--format sarif` (GitHub code scanning) | The rule's help says how to report; each result carries its prefilled link in `properties.falsePositiveUrl`. | | `--format json` | `tool.falsePositives` is the form; build per-finding links with `falsePositiveUrl()` from `src/format/fp-link.ts`. | | The editor (`staleproof lsp`, VS Code, Cursor, Zed) | A quick fix on every diagnostic: **Staleproof: report a false positive**, which opens the prefilled form. | | staleproof.dev (audit, pull request check) | `False positive?` on each finding row. | The form asks for the rule, `owner/name@sha` (or "local working tree"), `file:line`, what Staleproof said, the line it flagged, why it is not wrong, the version, and whether you can share the repository publicly. ### Private repositories The issue is public, so a link made for a private repository carries only the rule, the shape of the message (names, paths and lists replaced by `…`) and the version: no path, no file text, no repository name. Staleproof knows a repository is private in the GitHub App (its private workspaces) and in the GitHub Action (the event says so). The editor and a local `staleproof lint` cannot tell; the form reminds you to leave out what you cannot share, and you see every field before you submit. Without a public repository we cannot copy files into a fixture, so a private report is reproduced by hand from the rule and the message. ## What happens next 1. **Reproduce.** `scripts/fp-to-fixture.ts --issue ` reads the issue, clones the public repository at the commit (shallow), and checks that the finding is there. 2. **Fixture.** It copies the page and the files the finding depends on (manifests, the link's target, the code an example calls, the files that name an env var) into `test/fixtures/fp/-/repo/`, drops every code file the finding does not need, and lints the fixture alone to confirm it shows the same finding. While the rule is not fixed the fixture is marked `pending`: the suite expects the finding back. 3. **Fix.** The rule is fixed. `test/fp-corpus.test.ts` then fails on the pending fixture ("no longer reproduces: remove `pending`"), the marker goes, and the fixture is a live guard. 4. **Release.** The fix ships with its guard; the issue gets the version that fixed it. From then on every change to Staleproof runs the fixture. A rule whose report cannot be fixed in the release that receives it becomes `beta` for that release. Maintainers can also make a fixture from explicit fields: ```bash npx tsx scripts/fp-to-fixture.ts --repo pmndrs/zustand --sha <40-hex sha> --rule env-unused \ --file CONTRIBUTING.md --line 82 --slug other-checkout --why "pmndrs/docs reads these, not zustand" ``` `--from ` reuses a clone at that commit, `--include ` adds a file the selection missed, `--code ` offers a large package's code (then trimmed to what the finding needs), `--max-severity info` accepts the finding back as an info note (a link that works on the docs site but not on GitHub), and `--guard` keeps an already fixed false positive fixed. ## How maturity works Every rule in [Lint rules](rules.md) is `stable` or `beta`. | | stable | beta | |---|---|---| | Severity | its own (`error`, `warning`, `info`) | `info`, marked `lowerConfidence` | | Listed | with the other findings | last, under "Lower confidence (beta rules)" | | `staleproof lint --fail-on`, `staleproof check --fail-on-predicted`, the Action's `fail-on`, the App's `fail-on`, the coding-agent hooks | can fail | never fails, at any level | | Docs audit score | deducted | never deducted | | Fingerprint | unchanged | unchanged | A beta finding is still worth a look, but check it before you act on it: on repositories they were not tuned on, some beta rules are wrong more often than right. Opt a rule in when you have checked it on your repository, in `.staleproof.toml` (or `[tool.staleproof.rules]` in `pyproject.toml`): ```toml [rules] "anchor-missing" = "warning" ``` Any level opts it in: its findings then count like every other rule's at that level (the docs audit counts a beta example check's lines at `warning` or `error`). `"off"` still turns a rule off. The beta rules today, and why (the counts are in [Lint rules](rules.md), from `src/lint/maturity.ts`): - **`path-missing`**: right on the repositories it was tuned on, but a held-out check on fresh repositories found no true finding: a removal recorded without a pull request reference, another repository's paths, and plans that name files not written yet. - **`line-out-of-range`**: it shares `path-missing`'s guards, and no true finding has been seen on any repository checked so far. - **`anchor-missing`**: docs sites that build heading ids their own way: MkDocs `attr_list` ids (`{ #id }`), hf doc-builder ids and slugs, generated API anchors (`[[autodoc]]`, `:::`). A heading found under another slugger is reported as an info note with no fix. The zod causes (a code block that closes an HTML comment on its fence line, links inside MDX comments) are fixed in 0.8.0. - **`cli-flag-unknown`**: the CLI model does not follow every way a CLI is put together yet: commander subcommands defined in other modules and mounted with `addCommand`, a launcher that forwards its arguments to other CLIs, two CLIs with one name. - **`symbol-missing`**: few true findings on real repositories so far; the false ones were in release notes and proposals, which name what is gone on purpose. - **`example-unknown-keyword`**: the receiver of a call is not always typed right (`with X() as y`, `await`), and pages of history (a `docs/v2` folder) still get judged. An overloaded call is judged only when every overload agrees. - **`example-too-many-arguments`**: still more false than true findings on fresh repositories. A function the page defines itself is the reader's code and is no longer judged (pmndrs/jotai's tutorial that rebuilds `atom`). - **`example-missing-argument`**: overloads that disagree, an arity that comes from a declared function type (`export const f: F = (a, b) => …`), functions the page defines (vitejs/vite's preset shadowed by an import). - **`example-missing-import`**: JavaScript and TypeScript exports are not modelled completely yet: `exports` maps and subpaths, `export *` chains, CommonJS and `export =` typings, unbuilt `dist/` entries, and types only a hand-written `index.d.ts` declares. - **`example-missing-attribute`**: an exported instance read as the module of the same name (trpc's `initTRPC`, TanStack/query's managers), and a method the class defines (vitejs/vite's `ModuleRunner.import`). Alternative tabs with no import of their own (zod's "Zod Mini" blocks) are no longer judged against the default tab's import. A stable rule can still get a report. Its fixture is then listed as open against the rule in `src/lint/maturity.ts`, and a release either ships the fix or ships the rule as beta. What a precision run finds and a release does not fix yet is written in the rule's evidence in [Lint rules](rules.md). The evidence for every rule, stable ones included, is in `src/lint/maturity.ts` and printed in [Lint rules](rules.md). A rule moves to stable when its fixtures pass and a precision run on fresh repositories, every finding hand-checked, finds it mostly right. --- # Drift Hall The Drift Hall is Staleproof's public precision record: `staleproof lint` run on about sixty popular open-source repositories that Staleproof's own tests were never tuned on, every error and warning checked by hand at a pinned commit, the confirmed drift listed with a link to the line, and every false positive counted by rule and by cause instead of hidden. This page explains how the corpus was chosen, how each finding was judged, how to read the numbers on [staleproof.dev/#/drift](https://staleproof.dev/#/drift), how the fixes are offered to maintainers, and how to reproduce the run. ## Why a hall A checker that is wrong costs more than one that misses: one false alarm teaches a team to ignore the next real one. Precision measured on repositories a tool's authors picked and tuned against says little, so the hall measures it on repositories Staleproof did not choose, and shows its own mistakes next to its findings. ## The corpus About sixty repositories (roughly thirty Python, twenty-five TypeScript and JavaScript, five Go), chosen for being popular, active and documented, with a preference for projects that keep AI agent files (`AGENTS.md`, `CLAUDE.md`, Cursor rules), since those are read on every agent session. None of them is one of the repositories in Staleproof's earlier precision runs. Each one is pinned to the head of its default branch on the day of the run, with its star count at that time; [scripts/drift-hall/repos.ts](../scripts/drift-hall/repos.ts) holds the list. ## The run [scripts/drift-hall/run.ts](../scripts/drift-hall/run.ts) fetches each repository at its recorded commit (shallow, blob-less, with enough history for `lint` to say where a deleted or renamed path went) and runs the graph-free check: ```bash npx -y staleproof lint --format json --fail-on never --repo ``` That covers paths, links, anchors, commands, the repository's own CLI and flags, environment variables, symbols, docstrings and the static checks of docs examples ([Lint](lint.md) has every rule). No graph, no network, no account; nothing is sent to the repositories. ## How each finding was judged Every error- and warning-level finding was read against the repository at the pinned commit: the doc line, and the thing it names (the file, the heading, the script, the signature, the export). Info-level findings were sampled. Each gets one verdict and a one-line reason: | Verdict | Meaning | |---|---| | confirmed | A reader following the docs where they are read hits the problem: the link 404s, the anchor does not land, the example raises, the path or command does not exist, the docstring names a parameter the function does not have | | false-positive | It works where it is read, or the text is not a claim about this repository | | unclear | The repository alone cannot settle it (for example, whether a docs framework falls back to English for an untranslated page) | The bar is the project's own docs site, not GitHub's rendering: a link that works on the site through its own heading slugger, site-root paths, redirects, generated API pages or included snippets is a **false positive of Staleproof**, not drift. Repeated findings with one cause (a generator writing a whole page of links the same wrong way) were checked one by one with a script that tests the cause for each finding, and read by eye in samples; anything the script could not settle was read individually. ## Reading the numbers The page leads with the **default view**: of the findings the current version reports at warning or error (what a check fails on, and what costs audit score), how many were drift. Beta rules report at info only; their precision is shown apart, and so is every warning and error of the first run ("before", 0.6.1), from before the guards. Default and beta follow the current rule maturity table: `path-missing` and `line-out-of-range` moved to beta after the held-out check below found them wrong on new repositories, so their findings count as beta even where the re-run, linted before the move, reported them at warning or error (the JSON's `note` says so; nothing is re-linted for it). **The guards were written against these same repositories.** Each false-positive cause found here became a guard (a fixture from the hall's case, with the true finding next to it), so the hall after the guards measures how well they fit this corpus, not how Staleproof does on a repository it has never seen. That is what the held-out check is for. - **Confirmed drift** counts findings, not causes: one generated page with 400 wrong links is 400 findings and one fix. The page groups each repository's findings by reason, so causes are visible. - **Precision** for a rule is confirmed out of confirmed plus false positives; unclear verdicts are left out of it. - **False positives** are grouped by cause (a site slugger Staleproof did not know, a snippet included into another page, a footnote read as a link, an example that calls the reader's own code). Each cause is a bug in a rule and goes to the rule's owner; when a release fixes one, the hall says so and counts its findings as found and fixed. - A finding in a file that is a link to another (`CLAUDE.md` pointing at `AGENTS.md`), or repeated in a docs site's frozen copy of the page for an older release (`docs/v1.15.28/`, `versioned_docs/version-2.30/`), is counted once, under the current page or else the newest copy. Those copies are judged on their merits: an example that was already broken when its release shipped is drift for that release's readers. ## The held-out check Eighteen more popular repositories (Python, TypeScript and JavaScript, Go), in none of the hall, the earlier precision runs or the rule-maturity sweep, were pinned and linted only after the guards were written, and every warning and error was hand-checked the same way ([scripts/drift-hall/heldout.ts](../scripts/drift-hall/heldout.ts)). Its precision, by rule and with the counts (the sample is small), is the number to quote; its false positives go back into the same loop. On it, `path-missing` was wrong 12 times out of 12 (removal records, another repository's paths, plans), which is why it is beta in 0.8.0. ## Fixes offered to maintainers For each repository with confirmed drift, one small docs pull request is drafted: at most about five fixes, each with a permalink to the line at the checked commit and the evidence (what the code or the target actually is), described as docs drift, ending with "Found with Staleproof". Fixes come from `staleproof fix` where Staleproof had a verified fix that was also right on review, otherwise by hand. Nothing is opened automatically: each pull request is reviewed and opened by a person, and the hall shows it as drafted, open or merged. ## The data [staleproof.dev/drift-hall.json](https://staleproof.dev/drift-hall.json) is the page's data, built by [scripts/drift-hall/build.ts](../scripts/drift-hall/build.ts): | Field | Holds | |---|---| | `generatedAt`, `staleproofVersion` | When it was built, and the Staleproof version that linted | | `corpus` | When the commits were resolved, how many repositories, by language | | `totals` | Repositories checked and with drift, files checked, findings judged, confirmed, false positives, unclear, infos sampled, false positives since fixed, pull requests drafted, open and merged | | `repos[]` | `repo`, `sha`, `stars`, `language`, `agentFiles`, `filesChecked`, `counts` (by verdict), `findings[]` (confirmed only: `rule`, `severity`, `file`, `line`, `message`, `fixAvailable`, `permalink` at the sha, `why`), `reasons`, `excluded` (short fingerprints of the false-positive and unclear findings) and `pr` (`status`: `drafted`, `opened` or `merged`, `url`, `title`) | | `precision` | The default view of the current version (`version`, `byRule`, `total`), its `beta` view (info only), `before` (every warning and error of the first run, which the repositories' counts are), `notRerun` and the note above | | `heldOut` | The held-out repositories (pinned sha, counts, confirmed findings with permalinks, excluded fingerprints), `byRule` and `total` | | `falsePositiveClasses[]` | Each cause: title, count, rules, an example, `open` or `fixed` (with the version) | Only confirmed findings are listed as findings; a test checks that no false-positive or unclear verdict appears among them. ## Reproduce it ```bash npx tsx scripts/drift-hall/resolve.ts # record each repository's commit and stars (gh CLI, read-only) npx tsx scripts/drift-hall/run.ts --clones /tmp/drift-hall --jobs 3 npx tsx scripts/drift-hall/verdicts.ts # compile the hand checks, list what is still unjudged npx tsx scripts/drift-hall/build.ts # write web/public/drift-hall.json # a newer Staleproof on the same commits, and the held-out set npx tsx scripts/drift-hall/run.ts --clones /tmp/drift-hall --no-fixes --raw /tmp/drift-next npx tsx scripts/drift-hall/compare.ts /tmp/drift-next # false positives gone, confirmed lost, new findings npx tsx scripts/drift-hall/run.ts --clones /tmp/held-out --no-fixes --held-out npx tsx scripts/drift-hall/verdicts.ts --held-out # list what is left to judge npx tsx scripts/drift-hall/build.ts --next /tmp/drift-next --next-version 0.8.0 ``` The hand checks themselves are judgments, kept with their one-line reasons; re-running the lint at the same commits gives the same findings for the same Staleproof version. ## Limits - One commit per repository, one day. Docs move; the permalinks keep the evidence at the commit that was checked. - Very large documentation trees can exceed lint's default memory or time on a shared machine; a repository that could not be linted is shown as not checked, never as clean. - Translations are judged on their merits when Staleproof reports them, though lint is meant to skip translated pages; links from a translation to a page that exists only in English stay unclear unless the docs framework's fallback is known. - The hall measures precision, not recall: drift Staleproof does not report is not counted. ## Related - [Lint docs and agent files](lint.md): every rule, and how each one avoids false positives. - [Fix stale docs](fix.md): the verified fixes the drafts start from. - [Docs audit](audit.md): the same checks for one repository, with a health score. --- # Tools reference This page is the reference for the fourteen tools that read and write the graph: the eight the agent chooses between, `write_decision` for people, and `check_pull_request`, `check_changes`, `docs_for_symbol`, `audit_docs` and `docs_patch` for MCP clients. It covers how every tool reports its work, who can call which tool, the ids they take, the usual order of calls for one pull request, and how errors come back; then each tool's parameters (generated from the code) and the shape of its result, its summary text, its side effects and the errors it raises. It is for MCP client authors, library users and anyone reading a trace. ## How tools work Every tool returns the same envelope: ```ts interface ToolResult { tool: string; // the tool's name summary: string; // one or two sentences, read first by the agent and shown in the UI data: T; // the structured result (shapes under Results below) queries: QueryTrace[]; // every Cypher statement the tool ran } interface QueryTrace { name: string; // the named query from src/graph/queries.ts, or 'adhoc' cypher: string; // the statement as sent params: Record; ms: number; // server-side execution time reported by FalkorDB rows: number; // rows returned } ``` Named queries are listed with their full text in [Graph model and Cypher](GRAPH-MODEL.md). Small lookups written inline in the code are traced as `adhoc`; the MCP output and the terminal trace leave those out. How the result reaches you depends on the caller: - **The agent** receives `{summary, data}` as JSON, cut at 24,000 characters. The trace stored on the check keeps the queries too. See [The agent](agent.md#tool-results). - **MCP clients** receive three text items: the summary, the data as indented JSON, and `Cypher run on FalkorDB:` followed by each named query with its row count and time (left out when there are none). `check_pull_request` returns two instead: the Markdown report, and `Stored as check . `. - **The CLI and the HTTP API** call the tools inside commands and endpoints and print or return their data. ### Who can call which tool | Tool | Agent | Graph mode | MCP | CLI | HTTP API | |---|---|---|---|---|---| | `check_pull_request` | no | it runs graph mode | yes | `check --no-agent --format markdown` (the same report) | `POST /api/mcp` | | `check_changes` | no | no | yes (`dir` on the stdio server only) | `check` with no pull request (the same analysis of a working tree) | `POST /api/mcp` (with `files`) | | `docs_for_symbol` | no | no | yes | `docs-for` | `GET /api/docs-for` | | `audit_docs` | no | no | yes | `audit` (which can also run examples first) | `GET /api/audit`, `POST /api/audit` | | `impact` | yes | yes | yes | `check`, `run --pr` | inside `POST /api/check` | | `clearance` | yes | yes | yes | `check` | inside `POST /api/check` | | `read_section` | yes | no | yes | no | `GET /api/sections?id=` | | `run_example` | yes | yes (up to 3) | yes, except on a hosted server | `run` | inside `POST /api/check` | | `decisions_for` | yes | no | yes | no | no | | `rank_sections` | yes | yes | yes | `check` | inside `POST /api/check` | | `propose_edit` | yes | no | yes | no | no | | `record_verdict` | yes | no | yes | no | no | | `write_decision` | never | no | yes | `decide` | `POST /api/decisions` | | `docs_patch` | no | no | yes | `patch` (same patch) | `GET /api/patch` (same patch) | The "Available to" line in each generated entry below lists the agent tools as available to graph mode "where it applies"; graph mode uses only the four marked yes here. Over the HTTP API every MCP tool is also reachable through `POST /api/mcp`. See [MCP server](mcp.md), [CLI](cli.md) and [HTTP API](http-api.md). ## Ids | Node | Format | Example | |---|---|---| | Symbol | `owner/repo:qualname` | `FalkorDB/falkordb-py:falkordb.asyncio.falkordb.FalkorDB.__init__` | | DocSection | `owner/repo:path#anchor` | `FalkorDB/falkordb-py:README.md#asynchronous-example` | | CodeExample | `owner/repo:path:index` | `FalkorDB/falkordb-py:README.md:4` | | PullRequest | URL | `https://github.com/FalkorDB/falkordb-py/pull/262` (also accepted: `owner/repo#n`, and URLs ending in `/files` or with a query) | | Decision, Verdict, Run | `dec-…`, `ver-…`, `run-…` | returned by the tools that create them | Section ids come from `impact` and `read_section`; example ids from `impact` (`examples` on each section) and `read_section`. A DocSection id for the text before a page's first heading ends in `#`. More in [Core concepts](concepts.md#ids). ## Typical call order for one pull request An MCP client usually starts with `check_pull_request`, which runs `impact`, `clearance` and `rank_sections` in one call (a graph-mode check) and returns the report; the steps below are for judging sections one by one. The agent decides its own order; this is the order the system prompt and the MCP server's instructions describe, and what a person driving an MCP client usually does: 1. `impact` with the pull request URL. If the summary says `call impact again with offset=40 for the rest`, page with `offset`. The first call ingests the pull request. 2. `read_section` for each impacted section, to see what it claims. 3. `decisions_for` on the section before editing it: a maintainer decision may say it stays as it is, and the history shows earlier verdicts. 4. `run_example` with the pull request URL for sections that have Python examples: the before/after outcome is the strongest evidence. 5. `propose_edit` for a section that is wrong, or `record_verdict` (`ok`, `governed`, `skipped`) for one that is not; several sections can share one `record_verdict`. 6. `docs_patch` (MCP) or `staleproof patch` for the diff. `clearance` (what no doc reaches) and `rank_sections` (which page matters most) are optional. For falkordb-py#262 the agent's order was impact → read_section → decisions_for → run_example → record_verdict. Before a pull request exists, an MCP client calls `docs_for_symbol` with the function it is about to change (to know which docs to update in the same change), then `check_changes` with the files it edited (to see which examples its edits break). With no change at all, `audit_docs` reports how the docs stand. These three write nothing to the graph. ## Errors A tool that cannot do its job throws an error with a message written for whoever called it. Nothing crashes the caller: - **In the agent loop**, the error goes back to the model as an error result for that tool call, the trace records it with `isError: true`, and the check continues. - **Over MCP**, the call returns `isError: true` with the message as its only text item. - **On the CLI**, the command prints `staleproof: ` and exits with code 1. - **Over HTTP**, endpoints answer 400, 404 or 500 with `{error}`; see [HTTP API](http-api.md). Input problems are caught before a tool runs, with the same rules in the agent loop and over MCP: | Problem | Agent loop | MCP | |---|---|---| | Unknown tool | `unknown tool ` | `unknown tool ` | | Parameter the tool does not have | ` has no parameter ; its parameters are ` | same | | Invalid value | `invalid input for : : ; …` | `invalid input: : ; …` | `pr_url` on `propose_edit` and `record_verdict` defaults to the pull request under check in agent mode. Over MCP there is no pull request under check, so it is required (`pr_url is required`), and verdicts written over MCP are not attached to any check. ## Tools ### `check_pull_request` Check a GitHub pull request for docs it can make false, in one call: the references in the docs and AI agent files it makes wrong (paths, links, commands, env vars; linted at both commits), every doc section and example the changed code reaches (with the evidence path), the changed symbols no doc reaches, ranked by PageRank, as a Markdown report. Start here; use impact/read_section/decisions_for to dig into one section. Available to MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | yes | https://github.com///pull/ | | `run_examples` | boolean | no | Also run up to 3 affected examples before and after the PR (local servers only; takes minutes) | Cypher it runs: `impact`, `clearance`, `rankSections` ([full text](GRAPH-MODEL.md)). ### `check_changes` Check edits that are not a pull request yet: which doc sections and examples they can make false (with the evidence path), which examples are predicted to break (their calls no longer fit the changed code), and which changed symbols no doc reaches. Send the new contents of the changed files (content null deletes a file); they are compared with the commit the graph indexed. Nothing is written to the graph. On a local server, pass dir (a git checkout) instead of files to check its working tree. Available to MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `repo` | string | no | owner/name of the repository the files belong to (needed with files) | | `files` | object[] | no | Changed files: repository-relative path and full new content (null when deleted) | | `dir` | string | no | Local servers only: a git checkout whose working tree to check (default: the server's directory) | | `base` | string | no | With dir: the commit or ref to compare against (default: merge base with the remote default branch) | Cypher it runs: `impactOfChanges`, `predictedCallsOfChanges`, `clearanceOfChanges`, `mismatchesFor` ([full text](GRAPH-MODEL.md)). ### `lint_docs` Lint the docs, READMEs and AI agent instruction files (AGENTS.md, CLAUDE.md, Cursor and Copilot rules) of a repository: every path, link, anchor, command, CLI flag, env var and docstring parameter they name that the repository does not have, with the exact line and where Staleproof looked. No graph and no index needed. Pass repo (owner/name of a public GitHub repository, optionally ref), or on a local server dir (a checkout); with dir, new_only keeps only findings the working tree introduced (absent at base, default HEAD when there are uncommitted changes, else the merge base with the default branch). Available to MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `repo` | string | no | owner/name of a GitHub repository to lint from its tarball | | `ref` | string | no | With repo: a branch, tag or commit (default: the default branch) | | `dir` | string | no | Local servers only: a checkout to lint | | `new_only` | boolean | no | With dir: only findings absent at the base commit | | `base` | string | no | With dir and new_only: the commit to compare with | | `rules` | string[] | no | Only these rule ids (see https://staleproof.dev/#/docs/rules) | | `files` | string[] | no | Only findings in these files or about them | ### `docs_for_symbol` Which doc sections and code examples depend on a symbol, in any repository, with the evidence path for each: symbol ← up to 3 callers ← doc. Call it before changing a function, class or method to know which docs to update in the same change. The symbol can be an id (owner/repo:pkg.mod.Class.method), a qualified name, a dotted suffix like Graph.query, or a bare name. Available to MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `symbol` | string (non-empty) | yes | owner/repo:pkg.mod.Class.method, pkg.mod.Class.method, Class.method or method | | `repos` | string[] | no | Only symbols defined in these repositories (owner/name) | Cypher it runs: `docsForSymbol`, `findSymbols`, `mismatchesFor` ([full text](GRAPH-MODEL.md)). ### `audit_docs` Audit the docs of indexed repositories with no pull request: a 0-100 health score, every example line that no longer fits the code it calls (unknown keywords, wrong argument counts, names that no longer exist, found statically across repositories), the references in the docs and AI agent files that no longer match the repository (paths, links, commands, env vars; agent files first), API coverage (public symbols no doc names or calls, most used first) and the latest example runs at the indexed commits. Available to MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `repos` | string[] | no | Repositories to audit as owner/name (default: every repository in the graph) | | `limit` | integer 0–200 | no | How many undocumented symbols to list (default 25) | Cypher it runs: `auditRepos`, `auditExamples`, `auditMismatches`, `auditRuns`, `auditSymbols`, `auditFindings` ([full text](GRAPH-MODEL.md)). ### `impact` Find every doc section and code example a pull request can falsify, in any repository of the organisation. Walks changed symbol ← callers (up to max_hops, across repositories) ← MENTIONS/USES ← doc. Returns impacted sections ordered by severity (breaking: removed/renamed; api: signature; behavior: body changed) with the evidence path for each. Call this first for a PR. At most 40 sections come back per call; when more are impacted, call again with offset to page. Available to the agent, MCP clients and graph mode. | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | yes | GitHub pull request URL, e.g. https://github.com/FalkorDB/falkordb-py/pull/262 | | `max_hops` | integer 0–5 | no | Maximum call hops between the changed symbol and the doc (default 3) | | `offset` | integer ≥ 0 | no | Skip this many sections (paging: 40 per call; default 0) | Cypher it runs: `impact`, `prChanges`, `predictedCalls`, `mismatchesFor`, `upsertPullRequest`, `upsertChangedIn`, `upsertMissingSymbols`, `lateLinkSections`, `lateLinkExamples` ([full text](GRAPH-MODEL.md)). ### `clearance` Proof of absence: the changed symbols that reach no doc section or example within 3 call hops in any repository. Each comes with a git grep command and its raw text-match count so a human can check the claim. Available to the agent, MCP clients and graph mode. | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | yes | GitHub pull request URL (`https://github.com///pull/`) | Cypher it runs: `clearance` ([full text](GRAPH-MODEL.md)). ### `read_section` Read the current Markdown of a doc section and its code examples, before deciding whether and how to edit it. Available to the agent and MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `section_id` | string | yes | DocSection id (`owner/repo:path#anchor`), as returned by impact | Cypher it runs: `readSection` ([full text](GRAPH-MODEL.md)). ### `run_example` Execute a docs code example in a sandbox (fresh virtualenv with the repositories installed from source, scratch FalkorDB on localhost:6379, no other network). With pr_url it runs twice — at the commit before the PR and at the PR — so "passes before, fails after" proves the PR broke the example. Examples needing LLM keys, local files or placeholders are recorded as skipped with the reason. Identical runs are answered from the graph. Available to the agent, MCP clients and graph mode. | Parameter | Type | Required | Description | |---|---|---|---| | `example_id` | string | yes | CodeExample id from impact (repo:path:index) | | `pr_url` | string | no | Run before and after this pull request | Cypher it runs: `exampleForRun`, `installSet`, `cachedRun`, `upsertRun`, `linkRunToPr` ([full text](GRAPH-MODEL.md)). ### `decisions_for` Before judging a doc section, read the maintainers' decisions that govern it (directly, or through any symbol it mentions or its examples use, within 2 call hops; superseded decisions resolve to the current one) and the verdicts Staleproof gave this section on earlier pull requests (with whether maintainers accepted them). A governing decision can mean the section is intentionally as it is: cite it instead of editing. Available to the agent and MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `target_id` | string | yes | DocSection id (repo:path#anchor) or Symbol id (repo:qualname) | Cypher it runs: `decisionsForSection`, `decisionsForSymbol`, `priorVerdicts` ([full text](GRAPH-MODEL.md)). ### `rank_sections` Order doc sections by how central their page is in the docs (PageRank over LINKS_TO, summed per page), so the most-read stale page is fixed first. Available to the agent, MCP clients and graph mode. | Parameter | Type | Required | Description | |---|---|---|---| | `section_ids` | string[] (≥ 1) | yes | DocSection ids from impact | Cypher it runs: `rankSections`, `rankSectionsByLinks` ([full text](GRAPH-MODEL.md)). ### `propose_edit` Propose replacement Markdown for a stale doc section (the whole section, heading line included, as read_section returned it). Refused unless the graph has a path from this pull request's changes to the section: Staleproof never edits a paragraph without one. Keep everything that is still true; change only what the PR falsified, or what makes an already-broken example fail (fails before and after: say so). Cite the runs that prove it. Available to the agent and MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | no | The pull request; defaults to the one under check in agent mode, required over MCP | | `section_id` | string | yes | DocSection id (`owner/repo:path#anchor`), as returned by impact | | `new_text` | string (non-empty) | yes | Replacement Markdown for the whole section, heading line included | | `reason` | string (≥ 3 chars) | yes | Why: what the pull request changed and what the section claims | | `cites` | string[] | no | Decision ids this conclusion relies on | | `runs` | string[] | no | Run ids (from run_example) that support it | Cypher it runs: `sectionReachedByPr`, `readSection`, `createVerdict` ([full text](GRAPH-MODEL.md)). ### `record_verdict` Close impacted sections without editing them: ok (the change does not falsify it — say why, e.g. the example passed before and after), governed (a decision says it stays as is — cite the decision id), or skipped (cannot be judged — say why). Pass several section_ids when one reason covers them all. Every impacted section needs a verdict or an edit before you finish. Available to the agent and MCP clients. | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | no | The pull request; defaults to the one under check in agent mode, required over MCP | | `section_id` | string | no | DocSection id (`owner/repo:path#anchor`), as returned by impact | | `section_ids` | string[] | no | Several sections sharing one verdict and reason | | `verdict` | `ok` \| `governed` \| `skipped` | yes | | | `reason` | string (≥ 3 chars) | yes | Why: what the pull request changed and what the section claims | | `cites` | string[] | no | Decision ids | | `runs` | string[] | no | Run ids that support the verdict | Cypher it runs: `sectionReachedByPr`, `createVerdict` ([full text](GRAPH-MODEL.md)). ### `write_decision` Record a maintainer decision ("the async client keeps the old kwargs until 2.0") and the doc sections or symbols it governs. Future checks cite it instead of re-flagging. Decisions come from people: call this only when a human asked for it. Available to MCP clients, the CLI (`decide`) and the HTTP API (`POST /api/decisions`); never the agent. | Parameter | Type | Required | Description | |---|---|---|---| | `text` | string (≥ 5 chars) | yes | The decision, in one sentence | | `governs` | string[] (≥ 1) | yes | DocSection and/or Symbol ids | | `source_url` | string | no | Where the decision was made (issue, PR comment, ADR) | | `author` | string | no | Who decided | | `supersedes` | string | no | Id of the decision this replaces | Cypher it runs: `createDecision`, `governSections`, `governSymbols`, `supersedeDecision` ([full text](GRAPH-MODEL.md)). ### `docs_patch` The docs fix for a checked pull request as a git-apply-able unified diff, plus a PR description with the evidence path behind each edited section. Built from the latest proposed edits. Available to MCP clients; the same patch comes from the CLI (`patch`) and the HTTP API (`GET /api/patch`). | Parameter | Type | Required | Description | |---|---|---|---| | `pr_url` | string | yes | GitHub pull request URL (`https://github.com///pull/`) | Cypher it runs: `verdictsForPr` ([full text](GRAPH-MODEL.md)). ## Results What each tool returns in `data`, the summary it writes, what it changes in the graph, and the errors it raises. The shapes are defined in [src/tools/](../src/tools/) and, for `check_pull_request` and `docs_patch`, [src/mcp/server.ts](../src/mcp/server.ts). ### `check_pull_request` result | Field | Meaning | |---|---| | `report` | The Markdown report, the same one the GitHub Action posts: the title, the impact summary, the examples run before and after, the impacted sections with their evidence paths in PageRank order, the cleared symbols and the latest verdicts ([format](github-action.md#the-comment)) | | `checkId` | The `Check` node stored for this call (`chk-…`) | | `status` | `done`, or `error` when `impact` failed | Summary: the graph-mode check's summary: the impact and clearance summaries, then `Example runs not requested.` (or the runs' outcomes with `run_examples`), then `Graph mode: fixed tool order and no verdicts; agent mode (a model) judges each section and proposes the edits.` Over MCP the client receives the report and `Stored as check . `. Side effects: a graph-mode check ([`runGraphCheck`](agent.md#graph-mode)): the pull request is ingested on the first call, a `Check` node with the trace is stored, and with `run_examples: true` up to 3 examples run before and after (Run nodes). The local stdio server first indexes the pull request's repository when the graph does not have it; a hosted server indexes it into its owner's workspace, and executes no examples. Errors: those of `impact`, and on a hosted server the [hosted limits](troubleshooting.md#hosted-service). ### `check_changes` result | Field | Meaning | |---|---| | `impact` | The shape of the [`impact` result](#impact-result), for the edits: `pr` describes them (`state: 'local'`, `url` a GitHub compare or tree link, `title` such as `2 edited files on acme/mini-client@a6214ef` or `Working tree of main against a6214ef`), `changed[]` the symbol-level changes, `sections[]` every impacted section (not paged), `predicted[]` and `mismatched[]` | | `cleared[]` | Changed symbols no doc reaches within 3 call hops, as in the [`clearance` result](#clearance-result) | With `files`, each file's new content is compared with the same path at the commit the graph indexed (`base` overrides it), read from a local clone when the server has one, else from `raw.githubusercontent.com`; `content: null` deletes the file, and a path not in that commit is a new file. With `dir` (stdio server only), the checkout's working tree is compared with `base`, else its merge base with the remote default branch, as `staleproof check` without a pull request does. Summary: ` changed symbol(s) reach(es) doc section(s) ( breaking, signature, behavior; in other repositories).`, then ` example(s) is/are predicted to break, the call no longer fitting the changed code.` and ` changed symbol(s) reach(es) no doc.` when they apply; or `These changes touch no Python symbol in ; no docs can be falsified through the code.` Side effects: none; it writes nothing to the graph (a server may index the repository first when the graph lacks it). Errors: `repo (owner/name) is required with files`; ` is not indexed in the "" graph`; `send the changed files: files: [{path, content}] with repo (this server cannot read your disk)` (`dir`, or neither, on an HTTP server); `cannot tell which GitHub repository is (origin: ); pass --repo owner/name`; git errors for a `dir` that is not a checkout; `GitHub raw @ failed: `. ### `docs_for_symbol` result | Field | Meaning | |---|---| | `query` | The symbol as given | | `symbols[]` | The symbols looked up, at most 5: `id`, `qualname`, `kind`, `repo`, `signature`, `url` (on the default branch) | | `ambiguous` | True when the name matched several symbols and none exactly; narrow it with a qualified name or `repos` | | `sections[]` | Every doc section or example script that reaches one of them: `section`, `heading`, `repo`, `url`, `evidence` (symbol first, doc last), `hops`, `confidence`, `examples` (ids of the examples on the path), `symbols` (which of the looked-up symbols reach it) | | `mismatched[]` | Lines in those examples that already do not fit the code: `example`, `url`, `line`, `kind`, `detail`, `call` | An id or qualified name that matches exactly wins over suffix and bare-name matches. Sections are ordered by hops, then repository and id. Summary: ` doc section(s) reach(es) ( name or call it directly; repositor(y|ies)).`, followed by ` line(s) in those examples already do not fit the code.` and ` The name is ambiguous: pass a qualified name or repos to narrow it.` when they apply; `No doc section or example reaches within 3 call hops: changing it cannot make a doc false through the code.`; or `No live symbol in the "" graph is named .` (not an error). For several symbols, `` reads ` symbols named ` (with `(of )` when more matched). No side effects. ### `audit_docs` result The [docs audit](audit.md) of the given repositories, or of every repository in the graph. | Field | Meaning | |---|---| | `org`, `repos[]` | The graph, and each audited repository: `name`, `sha`, `symbols`, `sections`, `indexVersion` | | `outdatedIndex` | True when a repository was indexed by a Staleproof older than 0.3 (no `MISMATCH` edges or export flags) | | `score`, `grade` | 0-100 and `A`-`F` ([the formula](audit.md#the-score)) | | `deductions[]` | `points` and `why` for each deduction | | `examples` | `total`, `python`, `linked` (calling at least one indexed symbol), `mismatched` (with at least one `MISMATCH` line) | | `mismatches[]` | Every example line that no longer fits the code: `example`, `url` (to the line), `repo`, `path`, `heading`, `line`, `kind`, `detail`, `call`, `symbol` | | `coverage` | `api` (public symbols), `documented`, `percent` | | `undocumented[]` | The most used public symbols no doc names or calls, at most `limit`: `qualname`, `repo`, `kind`, `signature`, `url`, `callers`, `crossRepoCallers` | | `runs[]`, `runCounts` | The latest run of each example at the indexed commits (`example`, `url`, `status`, `error`, `reason`), and the counts `pass`, `fail` (with timeouts), `skipped`, `error` | | `generatedAt` | Milliseconds since the epoch | Summary: `Docs health /100 () for repositories">.`, then ` of examples that call the code no longer fit it ( lines).` (or `All examples that call the code fit it.`), ` of public symbols (%) are named or called by a doc.`, ` of examples run at the indexed commits fail.` when runs are recorded, and the older-index sentence when `outdatedIndex`. Side effects: none; over MCP it runs no example. Errors: `not indexed in the "" graph: ` for a named repository the graph does not hold (the stdio server indexes it first). ### `impact` result | Field | Meaning | |---|---| | `pr` | `url`, `title`, `repo`, `state` (`merged`, `open` or `closed`), `before` and `after` (the commits compared), `docsTouched` (Markdown files the pull request itself edits) | | `changed[]` | Every changed symbol, `added` ones included: `symbol` (qualified name), `change` (the [change kind](concepts.md#change-kinds)), and when present `before` and `after` signatures and `renamedTo` | | `sections[]` | The current page of impacted sections, at most 40 (fields below) | | `totalSections` | Impacted sections across all pages | | `offset` | Index of the first section on this page | | `truncated` | True when more sections follow this page | | `counts` | `breaking`, `api`, `behavior`: sections per severity, over all pages | | `predicted[]` | Over all pages, the example lines [predicted to break](concepts.md#predicted-breaks): `example`, `url` (to the line), `section`, `repo`, `path`, `line` (in the doc file), `symbol`, `change`, `kind` (`removed`, `missing-import`, `unknown-keyword`, `too-many-arguments` or `missing-argument`), `detail`, `call` (as written) and `confidence` (`exact`, or `by-name` for a removed symbol linked by name) | | `mismatched[]` | Impacted example lines that already did not fit the code when it was indexed (`MISMATCH` edges), except those the predictions already report: `example`, `url`, `line`, `kind`, `detail`, `call` | Each impacted section: | Field | Meaning | |---|---| | `section` | The DocSection id, or the CodeExample id for an example script (which has no section) | | `heading` | The section's heading path | | `repo`, `path`, `url` | Where it is; `url` links to the doc node on GitHub at the indexed commit | | `severity` | `breaking`, `api` or `behavior`, from the most severe change that reaches it | | `evidence` | The [evidence path](concepts.md#evidence-path): changed symbol first, doc last | | `hops` | Edges on that path ([hops](concepts.md#hops)) | | `confidence` | `exact`, `by-name` or `late` ([confidence](concepts.md#confidence)) | | `changes[]` | Every change that reaches the section: `symbol`, `change`, `before`, `after` | | `examples[]` | Ids of the section's code examples the walk reached | | `crossRepo` | True when the section is in a different repository from the pull request | Summary: ` changed symbols (and added) reach doc sections ( breaking, signature, behavior; in other repositories).` The added count appears only when there are added symbols, and the wording agrees in number (`1 changed symbol reaches 1 doc section`). With predictions it continues ` examples are predicted to break, the call no longer fitting the changed code (, , , …).`, and with lines already out of date ` impacted example(s) already did not fit the code before this pull request.` With more than 40 sections, or an `offset`, it continues `Showing – of ; call impact again with offset= for the rest.` (or ends `Showing – of .` on the last page). A pull request that changed no symbol of the indexed code (Python, TypeScript or JavaScript) gives ` changed no Python symbols in ; no docs can be falsified through code.` Side effects: the first call for a pull request ingests it: the `PullRequest` node, `CHANGED_IN` edges, removed symbols recreated with `late` links ([Pull request ingestion](concepts.md#pull-request-ingestion)). With `git`, ingestion also fetches the pull request's repository and checks out its default branch in the local clone; the GitHub metadata of a merged pull request is cached under `STALEPROOF_HOME/cache`. Errors: - `not a GitHub pull request URL: "" (expected https://github.com///pull/)` - ` is not indexed in the "" graph. Run: staleproof index (plus the repositories that document it).` - `GitHub API rate limit reached (60 requests/hour without a token; set GITHUB_TOKEN to raise it). It resets in min.` - `GitHub: not found (a private repository needs GITHUB_TOKEN with read access)` (without the parenthesis when a token is set) - `GitHub failed: `, `git failed in : `, `merge commit has no parent` ### `clearance` result | Field | Meaning | |---|---| | `cleared[]` | Changed symbols (not `added`) with no doc within 3 call hops: `symbol` (qualified name), `name`, `repo`, `change`, `visibility`, `textMatches`, `grep` | | `docsRepos[]` | The repositories that have doc sections, which `textMatches` searched | `textMatches` is the number of lines matching `git grep -cw -- '*.md' '*.mdx' '*.rst'` in the local clones of `docsRepos` (0 for repositories with no local clone, so always 0 on serverless hosts). `grep` is the command to check it yourself: `git grep -nw -- '*.md' '*.mdx' '*.rst'`. Summary, one of: - `The PR changed no existing symbols; there is nothing to clear.` - `Every changed symbol reaches at least one doc; nothing is cleared.` - ` changed symbols reach no doc within 3 hops ( also have zero text matches in the docs; share a name with text in the docs but no path resolves to them).` Side effects: ingests the pull request if needed, as `impact` does. Errors: the same as `impact`'s ingestion errors. ### `read_section` result | Field | Meaning | |---|---| | `id`, `repo`, `path` | The section | | `heading` | Its heading path | | `body` | Its Markdown, heading line included (stored up to 8000 characters) | | `url` | GitHub link at the indexed commit | | `lineno`, `endLineno` | Its lines in the file | | `examples[]` | Its code examples, in order: `id`, `lang`, `code`, `lineno` | Summary: ` (, lines -, examples).` For example `falkordb-py > Usage > Asynchronous Example (README.md, lines 64-114, 1 examples).` An unknown id is not an error: `data` is `null` and the summary is `No doc section .` No side effects. ### `run_example` result | Field | Meaning | |---|---| | `example` | The CodeExample id | | `url` | Its GitHub link | | `verdict` | The [outcome](concepts.md#run-outcomes): `breaks`, `fixes`, `passes`, `already-broken`, `changed-failure`, `fails`, `skipped` or `error` | | `runs[]` | One record per run, `before` then `after` with a pull request, otherwise one `current` run | | `editedByPr` | `true` when the pull request edits the example's page and the after run used the page as the pull request leaves it ([details](example-runner.md#pull-requests-that-edit-the-page)) | Each run record: `id`, `example`, `status` (`pass`, `fail`, `timeout`, `skipped`, `error`), `reason`, `error` (last traceback line), `traceback`, `stdout`, `durationMs`, `stitched` (ids of earlier examples prepended), `installed` (`repo@sha` list), `side`, `cached` (answered from the graph) and `createdAt`. See [The Run node](example-runner.md#the-run-node). Summary, by outcome (commits shortened to 7 characters): | Outcome | Summary | |---|---| | `breaks` | `Passes at and fails at with : the PR breaks this example.` | | `fixes` | `Fails at and passes at : the PR fixes this example.` | | `passes` | `Passes before and after the PR (, ): no observable break.`, or `Passes at the indexed commits.` without a pull request | | `already-broken` | `Fails both before and after the PR with : broken already, not by this PR.` | | `changed-failure` | `Fails before () and after with a different error ().` | | `fails` | `Fails at the indexed commits: .` | | `skipped` | `Not run: .` | | `error` | `The sandbox could not be prepared: ` | ` The after run uses the page as this pull request edits it.` is appended when the pull request edits the example's page, and ` (recorded earlier; not re-run)` when a run was answered from the graph. If the `before` run is skipped, the `after` run is not attempted. Side effects: a `Run` node per side (unless an identical run is reused), with `OF` to the example and `AT` to the pull request; clones or fetches the repositories, builds a virtualenv, and flushes the scratch FalkorDB. See [Example runner](example-runner.md). Errors: `no code example `; ` has not been checked yet; call impact first` (the pull request is not in the graph yet); the pull request URL error. Problems inside the sandbox are not errors: they come back as runs with status `error`, `fail` or `timeout`. ### `decisions_for` result | Field | Meaning | |---|---| | `decisions[]` | Active decisions that apply: `id`, `text`, `source`, `author`, `createdAt`, `matched` (the decision the graph reached; differs from `id` when it was superseded), `via` (the section's heading path for a direct match, else the governed symbol's qualified name) | | `history[]` | For a section id only: its 5 most recent verdicts on any pull request: `id`, `verdict`, `status`, `reason`, `pr`, `title`, `createdAt` | Summary: ` active decision(s) govern(s) this: "" ([, supersedes ], via ); …` (text cut at 80 characters), or `No decision governs this.`, followed when there is history by `Earlier: [ ()] on //pull/, …`. The status is shown when it is not `proposed`. An id containing `#` is treated as a DocSection id, anything else as a Symbol id. No side effects; an unknown id returns no decisions. See [How decisions_for resolves decisions](memory.md#how-decisions_for-resolves-decisions). ### `rank_sections` result | Field | Meaning | |---|---| | `ranked[]` | The given sections that exist, most central page first: `id`, `heading`, `page` (`repo:path`), `pageScore`, `score` | | `method` | `pagerank`, or `in-degree` when the graph algorithm failed | With PageRank, `score` is the section's own score and `pageScore` the sum over its page. With the in-degree fallback, `pageScore` is the number of links into the page and `score` is 0. Summary: `Ranked sections by PageRank of their page; most central: ().` (`by inbound links` for the fallback), or `None of these ids are doc sections.` No side effects. It does not raise errors of its own: a failing PageRank call falls back. ### `propose_edit` result | Field | Meaning | |---|---| | `id` | The new Verdict id | | `evidence` | The shortest path from the pull request's changes to the section (or one of its examples) | Summary: `Edit proposed for
(evidence: ← … ← ).` Side effects: a `Verdict` with `verdict: 'stale'`, `status: 'proposed'`, `new_text`, `old_text` (the section body as stored), `evidence`, `confidence: 'path'` and the check id (empty over MCP), with `ABOUT`, `FOR`, `CITES` (existing decisions only) and `EVIDENCE` (existing runs only) edges. The edit reaches files only through the [docs patch](concepts.md#the-docs-patch). Errors: - `pr_url is required` (over MCP, without `pr_url`) - `refused: no path in the graph from 's changes to
. Staleproof never edits a paragraph without a path; record_verdict instead if it needs no change.` The path must have at most 3 call hops and start from a change other than `added`. - `new_text is identical to the current section; use record_verdict with ok` - `no doc section
or pull request in the graph` ### `record_verdict` result | Field | Meaning | |---|---| | `ids` | One new Verdict id per distinct section | | `evidence` | Per section id, the path from the pull request's changes, or `[]` when there is none | Summary: `Verdict recorded for section(s).` Side effects: one `Verdict` per section, `status: 'proposed'`, with `confidence: 'path'`, or `'none'` when no path reaches the section (unlike `propose_edit`, a missing path does not block a verdict), and the same edges as `propose_edit`. Errors: - `give section_id or section_ids` (reported as invalid input) - `pr_url is required` (over MCP, without `pr_url`) - `a governed verdict must cite the decision id(s)` - `no doc section
or pull request in the graph` ### `write_decision` result | Field | Meaning | |---|---| | `id` | The new Decision id (`dec-…`) | | `governed` | How many of the given ids matched a DocSection or Symbol | Summary: `Recorded decision governing node(s).`, with `, superseding ` before the period when `supersedes` is given. Side effects: a `Decision` (`status: 'active'`, `author` default `unknown`), `GOVERNS` edges to the matching sections and symbols, and with `supersedes`, a `SUPERSEDES` edge to the old decision, whose status becomes `superseded` (an unknown old id is ignored). See [Decisions and memory](memory.md). Errors: `none of is a DocSection or Symbol id; decision not recorded`. `POST /api/decisions` also limits `text` to 500 characters. ### `docs_patch` result | Field | Meaning | |---|---| | `diff` | A unified diff of every edited file, `git apply`-able in each docs repository | | `title` | `docs: update for # ()` | | `body` | The pull request description: per edited section, the reason, evidence path, runs and decisions; then the sections left unchanged; then any edits not applied | | `files[]` | `:` for each changed file | Summary: ` file(s) changed: :, ….` or `No proposed edits for this pull request yet.` It reports no queries. The patch is built from the latest verdict per section: `stale` verdicts with replacement text that were not rejected. Files are read at the indexed commit, from the local clone when there is one, otherwise from `raw.githubusercontent.com`. An edit whose section changed since indexing, or whose file is missing at that commit, is listed in the body under "Not applied" instead of being applied. No side effects. `GET /api/patch` returns the same patch with the verdicts and conflicts as separate fields; `staleproof patch` prints it. Errors: ` has not been checked`; `GitHub raw @ failed: `; `git show` errors from a local clone. ## Related - [Core concepts](concepts.md): the terms these results use - [The agent](agent.md): how the agent calls the tools and what it is told - [MCP server](mcp.md): connecting an MCP client to these tools - [Graph model and Cypher](GRAPH-MODEL.md): the full text of every query - [Example runner](example-runner.md): what `run_example` does underneath --- # Configuration `staleproof` is configured by environment variables and a few command-line flags; it has no configuration file of its own and does not load `.env` files. Nothing has to be set: with no variables, the CLI starts a local FalkorDB and names each graph after the repository's owner. A repository being indexed can carry a small file of its own, `.staleproof.toml`, to leave paths out or mark examples not to run. This page lists every variable the code reads, with its default, its meaning and the commands that use it, then the global CLI options and their precedence, the files kept under `STALEPROOF_HOME`, the repository configuration and example directives, and complete configurations for local development, FalkorDB Cloud with DeepSeek, CI and Vercel. It is for anyone setting up the CLI, the server, the GitHub Action or a deployment. ## The variables you usually set ```bash export FALKORDB_URL='falkor://localhost:6379' # the FalkorDB holding the org graph (this is the default) export STALEPROOF_ORG=acme # graph name, one per organisation (default: the repository's owner) export GITHUB_TOKEN= # private repositories; raises the GitHub API limit of 60 requests/hour export ANTHROPIC_API_KEY= # agent mode; without it, use graph mode (--no-agent) ``` Everything else has a working default. Agent mode on an Anthropic-compatible provider such as DeepSeek also needs `ANTHROPIC_BASE_URL` and `STALEPROOF_MODEL` ([below](#model-agent-mode)). ## Environment variables `loadConfig` ([src/config.ts](../src/config.ts)) reads most of them once, when a command starts. Every command except `db` loads it, so an invalid value (for example in `STALEPROOF_EFFORT`) stops every command, not only the ones that use it. Staleproof was first published as Lockstep (npm `lockstep-docs`). Every `STALEPROOF_*` variable is also read under its former name, `LOCKSTEP_*`, when the new one is unset, and an existing `~/.lockstep` is used as the home when `~/.staleproof` does not exist, so deployments and caches made under the old name keep working. | Variable | Default | Meaning | Used by | |---|---|---|---| | `FALKORDB_URL` | `falkor://localhost:6379` | The FalkorDB server holding the org graph. Unset (and no `--db`), the CLI starts a local server on that address when nothing answers there | every command except `db`; Vercel function; `npm run snapshot` | | `STALEPROOF_ORG` | the owner of the repository a command is about, else `falkordb` | Organisation, which names the graph. In a hosted server: the curated graph | same | | `STALEPROOF_HOME` | `~/.staleproof`; `/tmp/staleproof` when `VERCEL` is set | Clones, downloaded tarballs, GitHub cache, virtualenvs, local FalkorDB | `index`, `check`, `run`, `patch`, `mcp`, `serve`, `db` | | `GITHUB_TOKEN` | — | GitHub token for the API, git clones, tarball downloads and file reads, so private repositories work | indexing, pull request ingestion (`check`, `run --pr`, `serve`, `mcp`), the docs patch, hosted pull request reads (hosted downloads never send it, so a hosted server indexes public repositories only); required by `patch --open-pr` | | `GH_TOKEN` | — | Used when `GITHUB_TOKEN` is unset | same | | `STALEPROOF_INGEST` | — | `github`: work as if git were missing even when it is installed: download repositories as tarballs and ingest pull requests through the GitHub API | indexing, pull request ingestion | | `STALEPROOF_HOSTED` | Vercel function: on; `serve`: off | Hosted mode: any public repository is indexed on demand into a workspace graph of its own. The Vercel function turns it off only for `off`; `serve` turns it on only for `on` (or `--hosted`) | Vercel function, `serve` | | `STALEPROOF_AGENT_CHECKS_PER_DAY` | `50` | Agent checks a hosted server starts per day (UTC), across all callers; over it, `POST /api/check` with `mode: "agent"` answers 429 | Vercel function and `serve --hosted` | | `ANTHROPIC_API_KEY` | — | Model API key (Claude API or an Anthropic-compatible provider) | `check` (agent mode), `serve`, Vercel function | | `ANTHROPIC_BASE_URL` | Claude API | Model endpoint; a host other than `api.anthropic.com` switches to compatible mode | same | | `STALEPROOF_MODEL` | `claude-sonnet-5-5` | Agent model | same; `check --model` overrides | | `STALEPROOF_EFFORT` | `medium` | `low`, `medium`, `high`, `xhigh` or `max` | same; `check --effort` overrides | | `STALEPROOF_FALLBACKS` | on | `off` disables the server-side refusal fallback (Claude API only) | same | | `STALEPROOF_SCRATCH_URL` | — | A separate FalkorDB that docs examples run against (host and port only) | example runs: `run`, `check`, `serve`, `mcp` | | `STALEPROOF_SCRATCH_PORT` | `6390` | Port of the scratch server Staleproof starts itself, when `STALEPROOF_SCRATCH_URL` is unset | same | | `STALEPROOF_RUN_TIMEOUT_MS` | `30000` | Time limit of one example run, in milliseconds | same | | `STALEPROOF_CHECK_BUDGET_MS` | `240000` when `VERCEL` is set, else none | Wall-clock budget of one agent check in the server | `serve`, Vercel function (not `check`) | | `STALEPROOF_PRELOADED` | falkordb-py #262, #267, #276 | Comma-separated pull request URLs listed on the home page | `serve` (`--preload` overrides), Vercel function, `npm run snapshot`, `scripts/seed.sh` | | `STALEPROOF_HOOK` | — | `off`, `warn` or `block`: what the coding-agent hooks do, over `[hooks] stop` in the repository ([Hooks](#hooks)); `off` silences the edit hook too | `hook` | | `STALEPROOF_CACHE` | on | `off`: parse every file on every run instead of reading the local parse cache (`.staleproof/cache/`) | `lint`, `hook` | | `STALEPROOF_CACHE_DIR` | — | Keep the parse cache in this folder instead of the checkout's `.staleproof/cache/`; indexing uses the cache only when it is set (the GitHub Action sets it, and keeps the folder between runs) | `lint`, `hook`, `index`, `check`, `audit` | | `STALEPROOF_DIGEST_WEBHOOK` | — | The Slack, Discord or other webhook `staleproof digest` posts to when `--webhook` is not given; logged by host only ([Docs audit](audit.md#weekly-digest-to-slack-or-discord)) | `digest` | | `GITHUB_APP_ID` | — | The GitHub App's numeric id; with the two below it turns the App's webhook and cron on ([GitHub App](github-app.md#run-your-own-app)) | `serve`, Vercel | | `GITHUB_APP_PRIVATE_KEY` | — | The App's private key: a PEM, a PEM with `\n` on one line, or base64 | `serve`, Vercel | | `GITHUB_APP_WEBHOOK_SECRET` | — | Checks every webhook delivery's `X-Hub-Signature-256` | `serve`, Vercel | | `GITHUB_APP_SLUG` | — | Makes **Install the GitHub App** the primary button on `#/install` (via `/api/health`); without an App the Action's button is | `serve`, Vercel | | `CRON_SECRET` | — | Protects `GET /api/github/cron` (Vercel Cron sends it as a bearer token); without it the App's daily job is off | `serve`, Vercel | | `PORT` | `8080` | Port `serve` listens on | `serve` (`--port` overrides) | | `HOST` | `127.0.0.1` | Interface `serve` binds | `serve` (`--host` overrides) | | `VERCEL` | — | Set by Vercel; changes the defaults of `STALEPROOF_HOME` and `STALEPROOF_CHECK_BUDGET_MS` | all | | `USER` | — | Default reviewer name for `verdict --by` (else `maintainer`) | `verdict` | | `FALKORDBLITE_VERSION` | `0.10.0` | The `falkordblite` wheel that provides the local FalkorDB binaries | `db`, the managed scratch server | | `FACTS_OUT` | `docs/FACTS.md` | Where `scripts/seed.sh` writes FACTS.md | `scripts/seed.sh` | Credential detection also reads `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_PROFILE`, `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_IDENTITY_TOKEN`, `ANTHROPIC_IDENTITY_TOKEN_FILE`, `ANTHROPIC_CONFIG_DIR` and `XDG_CONFIG_HOME` ([details](#model-agent-mode)). The sandbox shim reads `STALEPROOF_SCRATCH_HOST`, `STALEPROOF_SCRATCH_PORT` and `STALEPROOF_DB_PORTS` inside the example process ([details](#example-runner)). Empty values: `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, `STALEPROOF_SCRATCH_URL`, `STALEPROOF_CHECK_BUDGET_MS` and `VERCEL` treat an empty string as unset. The others take it literally: an empty `GITHUB_TOKEN` hides `GH_TOKEN`, an empty `STALEPROOF_PRELOADED` lists no pull requests, an empty `STALEPROOF_ORG` fails with `invalid org name: ""`, an empty `STALEPROOF_EFFORT` fails validation, and an empty number becomes 0. Unset a variable instead of setting it to an empty string; note that Docker Compose's `${VAR:-}` passes an empty string. ### Graph and organisation **`FALKORDB_URL`** is passed to the `falkordb` client, which turns a leading `falkor` into `redis` and hands the URL to node-redis: | Form | Meaning | |---|---| | `falkor://host:port` | Plain TCP (the FalkorDB Cloud free tier endpoint is plain TCP) | | `falkors://host:port` | TLS | | `falkor://user:password@host:port` | With credentials; FalkorDB Cloud's user is `falkordb` | | `redis://...`, `rediss://...` | Accepted as is | The user name and password are URL-decoded, so percent-encode any reserved character in them (`@` is `%40`, `:` is `%3A`, `/` is `%2F`, `#` is `%23`, `%` is `%25`). Error messages show the URL with the password replaced by `***`. The connection times out after 5 seconds; after a dropped connection the client retries 4 times, waiting 200 ms and doubling up to 2 s, then gives up with `FalkorDB unreachable`. One server can hold any number of org graphs. **`STALEPROOF_ORG`** names the graph. It is slugged: trimmed, lowercased, every run of characters other than `a-z`, `0-9`, `_` and `-` replaced by `-`, leading and trailing `-` removed. `Acme Inc` becomes `acme-inc`. A name with nothing left fails with `invalid org name: ""`. Each organisation is a separate graph on the same server; indexing, checks and memory never cross between graphs. **The default graph.** With neither `--org` nor `STALEPROOF_ORG`, a CLI command uses the graph of the owner of the repository it is about, slugged: the first repository of `index`, the pull request of `check` and `patch`, the example id of `run`, the first `--governs` id of `decide`. `check https://github.com/encode/httpx/pull/` reads `encode`; FalkorDB's repositories still land in `falkordb`. Commands that name no repository (`mcp`, `serve`, `verdict`, `facts`) use `falkordb`. Set `STALEPROOF_ORG` when one graph holds repositories of several owners, so every command and the MCP server use the same one. **Without `FALKORDB_URL`**, a command that cannot reach `localhost:6379` starts a local FalkorDB there (the falkordblite server of `staleproof db start`, installed on first use) instead of failing. With `FALKORDB_URL` or `--db` set, an unreachable server is an error. **`STALEPROOF_HOME`** is where files are kept ([layout](#files-under-staleproof_home)). On Vercel the default is `/tmp/staleproof`, because a function's file system is read-only except `/tmp`. The Docker image sets it to `/data`. ### GitHub **`GITHUB_TOKEN`**, else **`GH_TOKEN`**, is the one GitHub credential. With it set, private repositories the token can read work everywhere a public one does: | Where | How the token is sent | |---|---| | `api.github.com` (pull request metadata and diffs, opening docs pull requests) | `Authorization: Bearer ` | | git clones and fetches (indexing, example installs) | Through git's environment configuration (`GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, `GIT_CONFIG_VALUE_0`), as an `http.https://github.com/.extraheader`, so only to `https://github.com/`, and never in the remote URL or on a command line. Not added when the environment already sets `GIT_CONFIG_COUNT` | | Tarball downloads (no git) | From the API's `repos///tarball/` endpoint instead of `codeload.github.com` | | `raw.githubusercontent.com` (file contents without a clone) | `Authorization: token ` | Without a token, GitHub allows 60 API requests per hour per IP address, and the error is `GitHub API rate limit reached (60 requests/hour without a token; set GITHUB_TOKEN to raise it). It resets in min.`; a 404 adds `(a private repository needs GITHUB_TOKEN with read access)`. - **What uses the API**: ingesting a pull request (once per pull request; later checks reuse the graph). With git, that is one request for the metadata, cached on disk for merged pull requests. Without git, or with `STALEPROOF_INGEST=github`, it is up to five (metadata, the commit range, the changed files and the file list at both commits); file contents come from `raw.githubusercontent.com`, which the code treats as outside the API limit. Indexing uses no API request with git (it clones over HTTPS) or without a token (the tarball comes from `codeload.github.com`); a tarball download with a token is one API request. - **`patch --open-pr`** refuses to run without a token (`opening a pull request needs GITHUB_TOKEN`). The token must be able to fork the docs repository, push a branch to the fork and open a pull request; for a classic token that is the `public_repo` scope. With `--in-repo` there is no fork: the token needs `contents: write` and `pull-requests: write` on the docs repository itself, which is what a GitHub Actions job token can be given for its own repository. - **A hosted server** downloads with its token, so it can index any repository that token reads, for whoever asks. Give it a fine-grained token limited to public repositories, read-only ([Deploy](DEPLOY.md#hosted-mode)). **`STALEPROOF_INGEST=github`** makes Staleproof work as it does where git is missing: `index` and indexing on first use download tarballs, and pull request ingestion reads commits through the GitHub API, the paths serverless hosts take automatically. Example runs still need git. ### Model (agent mode) | Variable | Effect | |---|---| | `ANTHROPIC_API_KEY` | Passed to the Anthropic SDK as the API key. Its presence turns on agent mode in `serve` | | `ANTHROPIC_BASE_URL` | Unset or empty: the Claude API. A URL whose host is not `api.anthropic.com` (or that does not parse) switches on compatible mode | | `STALEPROOF_MODEL` | Model name; default `claude-sonnet-5-5`. `check --model` overrides it | | `STALEPROOF_EFFORT` | `low`, `medium` (default), `high`, `xhigh` or `max`. Any other value fails every command except `db` with `STALEPROOF_EFFORT must be one of low, medium, high, xhigh, max`, even when `--effort` is also given. `check --effort` itself is not checked locally; an unknown value is sent to the model API as is | | `STALEPROOF_FALLBACKS` | Exactly `off` disables the server-side refusal fallback. It only matters on the Claude API: compatible mode never sends it, so there is no need to set it for DeepSeek | **Compatible mode** sends only the portable fields of the Messages API: `model`, `max_tokens`, `system` (as a plain string), `tools`, `messages`, `thinking` (`{type: "enabled", budget_tokens: min(16000, max_tokens - 1000)}`) and `output_config.effort`. It sends no beta flags, no refusal fallback, no prompt caching and no eager input streaming. It refuses a model name starting with `claude-` (any case), because a compatible provider would answer with its own model under that name and every trace would be mislabelled: ```text "claude-sonnet-5-5" is a Claude model name, but the model endpoint is an Anthropic-compatible provider, which would answer with its own model under that name; set STALEPROOF_MODEL to the provider's model (e.g. deepseek-v4-pro) ``` Setting `ANTHROPIC_BASE_URL` without `STALEPROOF_MODEL` therefore always hits this error. **Credentials** are found when any of these holds; otherwise agent mode fails with `the agent needs model API credentials: ...` and `serve` runs graph mode only: - `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_PROFILE` is set; - `ANTHROPIC_FEDERATION_RULE_ID` is set with `ANTHROPIC_IDENTITY_TOKEN` or `ANTHROPIC_IDENTITY_TOKEN_FILE`; - the Anthropic config directory exists: `ANTHROPIC_CONFIG_DIR`, else `$XDG_CONFIG_HOME/anthropic`, else `~/.config/anthropic` (created by `ant auth login`). Each model turn may produce up to 32,000 tokens. There is no variable for it; the library's `AnthropicModel` takes `maxTokens`. ### Example runner The runner installs the repositories at the commits before and after a pull request and runs a docs example against a scratch FalkorDB ([Example runner](example-runner.md)). | Variable | Effect | |---|---| | `STALEPROOF_SCRATCH_URL` | Use this server as the scratch database. Only the host and port are read (`[scheme://]host[:port]`, default port 6379); the scheme is ignored and TLS is not used. A URL with credentials is refused: `STALEPROOF_SCRATCH_URL must not carry credentials: docs examples connect to the scratch server without a password`. Staleproof does not start or stop this server | | `STALEPROOF_SCRATCH_PORT` | Without `STALEPROOF_SCRATCH_URL`, Staleproof starts its own scratch server on `127.0.0.1:` (default 6390) with `scripts/falkordb-local.sh`, the same falkordblite binaries as `staleproof db start` | | `STALEPROOF_RUN_TIMEOUT_MS` | Each run (before and after are separate runs) is killed after this many milliseconds and recorded as `timeout` with the reason `no exit after s`. Default 30000. Creating a virtualenv (300 s) and installing into it (600 s), and each step that builds a JavaScript/TypeScript package (600 s), have fixed limits | The scratch server is flushed before every run (`FLUSHALL`, then `GRAPH.UDF FLUSH`). Staleproof refuses to flush the server that holds the org graph: when the scratch host and port equal those of `FALKORDB_URL` (`localhost` and `127.0.0.1` count as the same host), each run ends with status `error` and the reason `refusing to flush :: it is the server holding the org graph (FALKORDB_URL). Point STALEPROOF_SCRATCH_URL / STALEPROOF_SCRATCH_PORT at a separate server.` Inside the sandbox, the example process inherits no environment. The runner sets `STALEPROOF_SCRATCH_HOST` and `STALEPROOF_SCRATCH_PORT` for the network shim, which sends connections to a local host on the ports in `STALEPROOF_DB_PORTS` (default `6379`) to the scratch server. Staleproof never sets `STALEPROOF_DB_PORTS`, and because nothing is inherited you cannot set it either: the remapped port is always 6379. The install step (`uv venv` and `uv pip install`, or `python3 -m venv` and `pip install`) runs the repositories' build code at the pull request's commits, so it gets an allow-listed environment, not the server's: only the variables matching `INSTALL_ENV` in [src/runner/sandbox.ts](../src/runner/sandbox.ts) are passed on. | Passed to the install step | Variables | |---|---| | Paths and user | `PATH`, `HOME`, `TMPDIR`, `TEMP`, `TMP`, `USER`, `LOGNAME`, `SHELL`, `XDG_CACHE_HOME`, `XDG_DATA_HOME` | | Locale and time | `LANG`, `LC_*`, `TZ` | | Package tools | every `UV_*` and `PIP_*` variable | | Proxies | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `ALL_PROXY` (and their lowercase forms) | | Certificates | `SSL_CERT_FILE`, `SSL_CERT_DIR`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` | | Windows | `SYSTEMROOT`, `COMSPEC`, `PATHEXT` | Everything else, including `FALKORDB_URL`, `ANTHROPIC_API_KEY` and `GITHUB_TOKEN`, is withheld. uv and pip settings such as `UV_INDEX_URL` or `PIP_INDEX_URL` (a package mirror) still apply, and the Docker image's `UV_PYTHON_INSTALL_DIR=/data/python` and `UV_CACHE_DIR=/data/uv-cache` reach uv the same way. Any other variable a repository's build reads is not set. JavaScript and TypeScript examples build their packages with npm, pnpm or yarn under the same allow-list ([Example runner](example-runner.md#building-the-packages-from-source)), plus every `npm_config_*` variable (either case, so `npm_config_registry` points at a mirror, as `UV_*` and `PIP_*` do for Python), with `npm_config_update_notifier`, `npm_config_fund` and `npm_config_audit` set to `false`. `HOME` is passed, so `~/.npmrc` applies too. ### Server | Variable | Effect | |---|---| | `PORT`, `HOST` | Address of `staleproof serve`; `--port` and `--host` override them. The Docker image sets `HOST=0.0.0.0` and `PORT=8080`. Not used on Vercel | | `STALEPROOF_PRELOADED` | Comma-separated pull request URLs listed on the home page and by `GET /api/prs`; spaces around entries are trimmed and empty entries dropped. `serve --preload ` overrides it. Unset: falkordb-py #262, #267 and #276. Every entry must be a pull request URL, or `GET /api/prs` answers 500 | | `STALEPROOF_CHECK_BUDGET_MS` | After this many milliseconds, an agent check in the server starts no new model turn and ends `incomplete`, listing the sections still open. Default 240000 on Vercel (whose function limit is 300 s in [vercel.json](../vercel.json)), none elsewhere. `staleproof check` ignores it. When a hosted server indexes first, indexing counts against it, but the agent always gets at least 45 seconds. `0` ends every agent check before its first turn | | `STALEPROOF_HOSTED` | Hosted mode ([Deploy](DEPLOY.md#hosted-mode)). In the Vercel function it is on unless the value is `off`; for `staleproof serve` it is off unless the value is `on` or `--hosted` is given | | `STALEPROOF_AGENT_CHECKS_PER_DAY` | In hosted mode (the Vercel function and `serve --hosted`): how many agent checks start per day (UTC) across the service, default 50 | | `VERCEL` | Any non-empty value. Vercel sets it, which moves `STALEPROOF_HOME` to `/tmp/staleproof` and sets the 240 s check budget | The HTTP rate limit (30 POST requests per client per hour) has no variable; see [HTTP API](http-api.md#rate-limits). The hosted workspace limits have none either; they are `DEFAULT_HOSTED_LIMITS` in [src/hosted/workspaces.ts](../src/hosted/workspaces.ts). ### Scripts and helpers - **`FALKORDBLITE_VERSION`** (default `0.10.0`) is the version of the `falkordblite` Python wheel that `scripts/falkordb-local.sh` installs into `$STALEPROOF_HOME/falkordb/venv` (with uv, else `python3 -m venv` and pip). It provides `redis-server` and the FalkorDB module for `staleproof db start` and for the managed scratch server. That build is FalkorDB 4.18.3 ([version notes](troubleshooting.md#falkordb-version-notes)). - **`FACTS_OUT`** is where `scripts/seed.sh` writes FACTS.md (default `docs/FACTS.md`, relative to the working directory). - **`USER`** is the default for `verdict --by`. ## Global CLI options and precedence | Option | Overrides | Default | |---|---|---| | `--org ` | `STALEPROOF_ORG` | the owner of the repository the command is about, else `falkordb` | | `--db ` | `FALKORDB_URL` | `falkor://localhost:6379`, started locally when nothing answers | Both may come before or after the command (`staleproof --org acme index ...` and `staleproof index ... --org acme` are the same). The general rule is flag, then environment variable, then default: | Setting | Flag | Variable | Default | |---|---|---|---| | Graph name | `--org` | `STALEPROOF_ORG` | the repository's owner, else `falkordb` | | FalkorDB URL | `--db` | `FALKORDB_URL` | `falkor://localhost:6379` | | Agent model | `check --model` | `STALEPROOF_MODEL` | `claude-sonnet-5-5` | | Effort | `check --effort` | `STALEPROOF_EFFORT` | `medium` | | Server port | `serve --port` | `PORT` | `8080` | | Server interface | `serve --host` | `HOST` | `127.0.0.1` | | Preloaded pull requests | `serve --preload` | `STALEPROOF_PRELOADED` | falkordb-py #262, #267, #276 | | Hosted mode | `serve --hosted` | `STALEPROOF_HOSTED` | off for `serve`, on in the Vercel function | | Reviewer | `verdict --by` | `USER` | `maintainer` | | Local FalkorDB port | `db --port` | — | `6379` | The other limits are flags only: `check --max-steps` (40), `check --no-agent --max-hops` (3) and `--runs` (3). `check --with` and `--no-index` control indexing on first use. Every command and option is in [CLI](cli.md). ## Files under STALEPROOF_HOME ```text ~/.staleproof/ STALEPROOF_HOME repos/// blob-less git clones: indexing, PR ingestion, example installs, the docs patch, clearance's text-match counts tarballs//// files extracted from a downloaded tarball (no git); hosted mode deletes them once the repository is indexed cache/github///pulls/.json pull request metadata (read back only for merged PRs) venvs// one virtualenv per set of repo@sha, marked done by .staleproof-ok node-envs// one Node project per set of repo@sha and imported packages (JS/TS runs) src//__/ sources extracted while a virtualenv builds; removed after a successful build src/node-/__/ sources JS/TS packages are built and packed from; removed afterwards falkordb/venv/ falkordblite, installed on first `db start` or scratch start falkordb/data-/ data (RDB snapshots), redis.log and redis.pid of each local server ``` - **`falkordb/data-6379`** holds the org graph when you run `staleproof db start`, including every verdict, decision and run. Deleting it deletes the graph. `data-6390` is the scratch server, which is flushed before every run anyway. - **`venvs/`, `node-envs/` and `src/`** can be deleted at any time; virtualenvs and Node projects are rebuilt on the next run that needs them. - **`repos/`** can be deleted; clones come back on the next index or check. Until then, `clearance` reports 0 raw text matches and the docs patch reads files from GitHub instead. - **`tarballs/`** can be deleted; it is only read while indexing. - **`cache/`** can be deleted; metadata is fetched again. - Each example run also uses a temporary directory, `$TMPDIR/staleproof-run-*`, removed when the run ends. ## Repository configuration A repository can tell the indexer what to leave out, in `.staleproof.toml` at its root: ```toml ignore = ["docs/legacy/**", "CHANGELOG.md"] # never indexed skip-examples = ["docs/v1/**"] # indexed and linked, never run or checked ``` or in the `[tool.staleproof]` table of its root `pyproject.toml`: ```toml [tool.staleproof] ignore = ["docs/legacy/**"] skip-examples = ["docs/v1/**"] ``` | Key | Type | Effect | |---|---|---| | `ignore` | list of globs | Matching files are not indexed at all: no symbols, sections or examples. | | `skip-examples` | list of globs | Examples on matching pages, and matching example scripts, are indexed and linked to the code as usual, so a pull request check still lists them, but they are never run, statically checked, predicted to break or counted as mismatched by the audit (as if each had a `skip` directive). Versioned copies of older docs kept next to the current ones (`docs-v3/` beside `docs/`, `versioned_docs/`) and translated READMEs (`README_KO.md`) are treated this way without being listed ([Indexing](indexing.md#static-checks-of-examples)). | `staleproof lint` reads two more tables (in `pyproject.toml`, `[tool.staleproof.lint]` and `[tool.staleproof.rules]`): ```toml [lint] include = ["guides/**/*.txt"] # extra files to check exclude = ["docs/archive/**"] # never checked (on top of `ignore`) agent-files = ["prompts/*.md"] # extra AI agent instruction files [rules] "anchor-missing" = "error" # off | info | warning | error ``` | Key | Type | Effect | |---|---|---| | `lint.include` | list of globs | Files checked by `staleproof lint` besides the docs, READMEs and agent files | | `lint.exclude` | list of globs | Files `staleproof lint` never checks | | `lint.agent-files` | list of globs | Files reported under "AI agent instructions" (listed first, and checked strictly: see [Lint](lint.md#precision-first)) | | `rules.` | `off`, `info`, `warning` or `error` | Turn a [rule](rules.md) off or change its severity. Setting a beta rule to any level opts it in: its findings count like any other rule's at that level (the [audit](audit.md#the-score) counts its example lines at `warning` or `error`) | `staleproof fix`, the suggestions and the fix pull request follow the same settings: a rule turned `"off"` is neither reported nor fixed, and files under `ignore`, `[lint] exclude` or a `` comment are never edited. Fixes are never made on migration guides, release notes and old versions' docs (they name what is gone on purpose). - **Which file**: `.staleproof.toml` when it exists, even if empty; otherwise `[tool.staleproof]` when it is present. Unknown keys and values of the wrong type are reported as warnings (`staleproof lint` prints them) and ignored. - **Globs** are matched against repository-relative paths, gitignore style: `**` matches any number of directories, `*` anything within one path segment, `?` one character; a leading `./` or `/` is dropped; a pattern also matches everything under it, so `docs/v1` covers `docs/v1/guide.md` (but not `docs/v10/`). - **When**: read at index time. After editing it, index the repository again. - **Errors** stop the index: a key that is not a list of strings fails with `: ignore must be a list of glob strings` (or `skip-examples`), and a file that is not valid TOML with the parser's message. - **On hosts without git** (tarball downloads: the hosted service, Vercel, `STALEPROOF_INGEST=github`), hidden paths are not extracted except a root `.staleproof.toml`, so both files are read there too. ### Audit ```toml [audit] mention-owners = true # owners in the audit issue and the PR comment become @-mentions ``` Without it, owners from CODEOWNERS are shown as code and nobody is notified ([Docs audit](audit.md#owners-of-the-docs-to-fix)). ### Hooks The coding-agent hooks (the [Claude Code plugin](claude-code.md), and the hooks `staleproof init --claude` or `--cursor` writes) read one more table, `[hooks]` (in `pyproject.toml`, `[tool.staleproof.hooks]`): ```toml [hooks] stop = "block" # block | warn | off ``` | Key | Values | Effect | |---|---|---| | `hooks.stop` | `block` (default), `warn` or `off` | What the hook before an agent stops does with the docs references the session made wrong: ask the agent to fix them (once), only show them to you, or nothing. `STALEPROOF_HOOK` overrides it | The hooks keep a parse cache and their state in `.staleproof/cache/` at the top of the checkout ([Claude Code plugin: how it works](claude-code.md#how-it-works)); the folder ignores itself in git and is safe to delete. ### GitHub App The [GitHub App](github-app.md#configuration) reads `[app]` (in `pyproject.toml`, `[tool.staleproof.app]`) from the default branch: | Key | Values | Effect | |---|---|---| | `app.comment` | `findings` (default), `always`, `never` | When the App comments on a pull request | | `app.fail-on` | `never` (default), `findings`, `predicted` | When its check run fails | | `app.suggestions` | `true` (default) / `false` | Verified fixes as review suggestions | | `app.drafts` | `false` (default) / `true` | Check draft pull requests | | `app.also-index` | list of `owner/name` | Repositories indexed with this one | | `app.fix-prs` | `false` (default) / `true` | Daily docs fix pull request | | `app.audit-issue` | `false` (default) / `true` | Daily audit issue | | `app.run-examples` | `true` (default) / `false` | Run the affected docs examples before and after in a sandbox, when the server has example runs switched on (not yet on staleproof.dev) | ### Example directives One example at a time is marked in the docs themselves, with a comment on the line before it (blank lines in between are fine) or a word in its fence's info string: | Directive | Means | |---|---| | ``, `{/* staleproof: skip */}` (MDX), fence word `skip` or `no-run` | Not runnable here (a long-running server, a cloud account): never run or statically checked | | ``, fence word `expect-error` | Fails on purpose (it shows an error): never run or statically checked, because a failure proves nothing | | ``, `` (Sybil) | Read as `skip`, so docs already marked for those tools need no second marker | | `` | Read as `expect-error` | | `.. staleproof: skip`, `.. staleproof: expect-error`, `.. skip: next` (reStructuredText) | The same, as a comment before the block | A marked example stays linked to the code it calls: a pull request check still lists its section, and its definitions still serve the examples after it on the page. `run_example` answers `Not run: the docs or the repository config mark this example skip.` (or `… failing on purpose (expect-error), so a run proves nothing.`) without running anything. [Indexing](indexing.md#example-directives) has every accepted spelling. ## Configurations that work together ### Local development ```bash npx -y staleproof db start # FalkorDB on 127.0.0.1:6379, data in ~/.staleproof/falkordb/data-6379 export GITHUB_TOKEN= # optional: more than 60 GitHub requests per hour export ANTHROPIC_API_KEY= # optional: agent mode npx -y staleproof index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/262 --no-agent npx -y staleproof serve ``` No other variable is needed: the `FALKORDB_URL` default matches `db start` (and without `db start`, the first command starts the same server), the graph is named after the repositories' owner (`falkordb`), and the scratch server for example runs starts by itself on port 6390. The host needs `git`, plus `uv` or `python3` (uv is recommended: it pins Python 3.12 for the sandbox; with `python3` the version is whatever the host has). ### FalkorDB Cloud and DeepSeek ```bash export FALKORDB_URL='falkor://falkordb:@:' # falkors:// if the endpoint has TLS export STALEPROOF_ORG=falkordb export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic export ANTHROPIC_API_KEY= export STALEPROOF_MODEL=deepseek-v4-pro export GITHUB_TOKEN= npx -y staleproof check https://github.com/FalkorDB/falkordb-py/pull/267 ``` The live demo's agent mode uses this provider and model. Leave `STALEPROOF_SCRATCH_URL` unset so example runs use the local scratch server on 127.0.0.1:6390, never the cloud database. `STALEPROOF_FALLBACKS` needs no setting: compatible mode never sends the fallback. ### CI (GitHub Action) The [GitHub Action](github-action.md) needs no variables: it starts FalkorDB in the job and sets `FALKORDB_URL=falkor://localhost:6379` and `STALEPROOF_SCRATCH_URL=falkor://localhost:6390` for its own containers (or lets the CLI start falkordblite when the runner has no Docker). Its inputs set the rest: `github-token` (default: the workflow's token) becomes `GITHUB_TOKEN`, `anthropic-api-key` becomes `ANTHROPIC_API_KEY`, `base-url` becomes `ANTHROPIC_BASE_URL`, `model` becomes `STALEPROOF_MODEL`, and with `falkordb-url` set, `falkordb-url` becomes `FALKORDB_URL` and `org` becomes `STALEPROOF_ORG`. With an empty key the action runs `check --no-agent --format markdown`: ```yaml jobs: docs: runs-on: ubuntu-latest steps: - uses: rajkaria/staleproof@v1 with: anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} base-url: https://api.deepseek.com/anthropic # only for an Anthropic-compatible provider model: deepseek-v4-pro ``` The workflow also needs `permissions: pull-requests: write` to post the comment ([examples/staleproof.yml](../examples/staleproof.yml)). On the runner, `STALEPROOF_HOME` is the default `~/.staleproof`, which starts empty on every job. ### Vercel ```bash vercel env add FALKORDB_URL production # required vercel env add GITHUB_TOKEN production # recommended: a fine-grained token, public repositories, read-only vercel env add ANTHROPIC_API_KEY production # optional: agent mode vercel env add ANTHROPIC_BASE_URL production # with STALEPROOF_MODEL, for an Anthropic-compatible provider vercel env add STALEPROOF_MODEL production vercel env add STALEPROOF_PRELOADED production # optional vercel env add STALEPROOF_AGENT_CHECKS_PER_DAY production # optional, default 50 vercel env add STALEPROOF_HOSTED production # optional: `off` serves the curated graph only vercel env add GITHUB_APP_ID production # optional: the GitHub App (scripts/github-app-register.ts writes .env.app) vercel env add GITHUB_APP_PRIVATE_KEY production vercel env add GITHUB_APP_WEBHOOK_SECRET production vercel env add GITHUB_APP_SLUG production vercel env add CRON_SECRET production # the App's daily job (vercel.json crons) ``` Vercel sets `VERCEL`, so `STALEPROOF_HOME` is `/tmp/staleproof` and agent checks stop at 240 s. Hosted mode is on unless `STALEPROOF_HOSTED=off`. `PORT`, `HOST` and the scratch variables are not used: the function serves no static files and executes no examples. [Deploy](DEPLOY.md#vercel) has the full procedure. ## Related - [CLI](cli.md): every command and flag - [Deploy](DEPLOY.md): Vercel, Docker Compose, FalkorDB Cloud and a production checklist - [Example runner](example-runner.md): the sandbox and the scratch server - [The agent](agent.md): model providers, budgets and graph mode - [Troubleshooting and FAQ](troubleshooting.md) --- # Graph model Generated by `npm run docs` from `src/graph/queries.ts`: every Cypher statement Staleproof sends to FalkorDB, named, with what it is for. One graph per organisation; the graph name is the org slug. ## Indexes - `CREATE INDEX FOR (n:Repo) ON (n.name)` - `CREATE INDEX FOR (n:File) ON (n.id)` - `CREATE INDEX FOR (n:Symbol) ON (n.id)` - `CREATE INDEX FOR (n:Symbol) ON (n.name)` - `CREATE INDEX FOR (n:Symbol) ON (n.repo)` - `CREATE INDEX FOR (n:DocSection) ON (n.id)` - `CREATE INDEX FOR (n:DocSection) ON (n.repo)` - `CREATE INDEX FOR (n:CodeExample) ON (n.id)` - `CREATE INDEX FOR (n:CodeExample) ON (n.repo)` - `CREATE INDEX FOR (n:PullRequest) ON (n.url)` - `CREATE INDEX FOR (n:Decision) ON (n.id)` - `CREATE INDEX FOR (n:Verdict) ON (n.id)` - `CREATE INDEX FOR (n:Verdict) ON (n.check)` - `CREATE INDEX FOR (n:Run) ON (n.id)` - `CREATE INDEX FOR (n:Run) ON (n.key)` - `CREATE INDEX FOR (n:Check) ON (n.id)` - `CREATE INDEX FOR (n:Finding) ON (n.id)` - `CREATE INDEX FOR (n:Finding) ON (n.repo)` ## Agent tools **`impact`** — Every doc section and code example a pull request reaches: changed symbol ← up to 3 callers (any repository) ← doc. The path is the evidence. ```cypher MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' MATCH p = (s)<-[:CALLS*0..3]-(t:Symbol)<-[:MENTIONS|USES]-(d) WHERE (d:DocSection OR d:CodeExample) AND d.status = 'live' OPTIONAL MATCH (d)-[:IN_SECTION]->(sec:DocSection) RETURN s.qualname AS changed, c.kind AS change, c.before AS before, c.after AS after, t.qualname AS reached, labels(d)[0] AS kind, d.id AS doc, d.repo AS repo, d.path AS path, coalesce(sec.id, d.id) AS section, coalesce(sec.heading_path, d.heading_path, d.path) AS heading, d.url AS url, length(p) AS hops, [n IN nodes(p) | coalesce(n.qualname, n.id)] AS evidence, [x IN relationships(p) | coalesce(x.resolved, 'exact')] AS resolution ORDER BY hops, repo, doc ``` **`clearance`** — Proof of absence: changed symbols with no doc section or example within 3 call hops in any repository. ```cypher MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' OPTIONAL MATCH p = (s)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-() WITH s, c, count(p) AS reach WHERE reach = 0 RETURN s.qualname AS symbol, s.name AS name, s.repo AS repo, c.kind AS change, s.visibility AS visibility ORDER BY symbol ``` **`prChanges`** — What a pull request changed, symbol by symbol. ```cypher MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) RETURN s.qualname AS symbol, s.kind AS kind, c.kind AS change, c.before AS before, c.after AS after, c.renamed_to AS renamedTo, s.repo AS repo, s.path AS path, s.status AS status ORDER BY change, symbol ``` **`rankSections`** — Order doc sections by how central their page is: PageRank over LINKS_TO, summed per page. ```cypher CALL algo.pageRank('DocSection', 'LINKS_TO') YIELD node, score WITH node.repo + ':' + node.path AS page, node.id AS id, node.heading_path AS heading, score WITH page, sum(score) AS pageScore, collect({id: id, heading: heading, score: score}) AS sections UNWIND sections AS sec WITH page, pageScore, sec WHERE sec.id IN $ids RETURN sec.id AS id, sec.heading AS heading, page, pageScore, sec.score AS score ORDER BY pageScore DESC, score DESC ``` **`rankSectionsByLinks`** — Fallback ranking when graph algorithms are unavailable: inbound links per page. ```cypher MATCH (d:DocSection) WHERE d.id IN $ids OPTIONAL MATCH (other:DocSection {repo: d.repo, path: d.path})<-[l:LINKS_TO]-() RETURN d.id AS id, d.heading_path AS heading, d.repo + ':' + d.path AS page, count(l) AS pageScore, 0 AS score ORDER BY pageScore DESC ``` **`readSection`** — The current text of a doc section, its code examples and its source link. ```cypher MATCH (d:DocSection {id: $id}) OPTIONAL MATCH (e:CodeExample)-[:IN_SECTION]->(d) WITH d, e ORDER BY e.index RETURN d.id AS id, d.repo AS repo, d.path AS path, d.heading_path AS heading, d.body AS body, d.url AS url, d.lineno AS lineno, d.end_lineno AS endLineno, collect(CASE WHEN e IS NULL THEN NULL ELSE {id: e.id, lang: e.lang, code: e.code, lineno: e.lineno} END) AS examples ``` **`exampleForRun`** — A code example, the repositories its calls resolve into, and the earlier examples on its page (for stitching fragments). ```cypher MATCH (e:CodeExample {id: $id}) OPTIONAL MATCH (e)-[:USES]->(s:Symbol) WITH e, collect(DISTINCT s.repo) AS usedRepos OPTIONAL MATCH (prev:CodeExample {repo: e.repo, path: e.path, status: 'live'}) WHERE prev.index < e.index WITH e, usedRepos, prev ORDER BY prev.index RETURN e.id AS id, e.lang AS lang, e.code AS code, e.repo AS repo, e.path AS path, e.url AS url, e.directive AS directive, usedRepos, collect(CASE WHEN prev IS NULL THEN NULL ELSE {id: prev.id, lang: prev.lang, code: prev.code} END) AS earlier ``` **`installSet`** — Repositories to install for a run: the given ones plus their in-org dependencies (DEPENDS_ON), code repositories only. ```cypher MATCH (r:Repo) WHERE r.name IN $repos OPTIONAL MATCH dep = (r)-[:DEPENDS_ON*1..3]->(d:Repo) WITH collect(DISTINCT r) + collect(DISTINCT d) AS repos UNWIND repos AS repo WITH DISTINCT repo WHERE repo IS NOT NULL AND size(repo.packages) > 0 RETURN repo.name AS repo, repo.sha AS sha ORDER BY repo ``` **`decisionsForSection`** — Active decisions governing a doc section directly, or any symbol its prose or examples reach within 2 call hops; superseded decisions resolve to their successor. ```cypher MATCH (d:DocSection {id: $id}) OPTIONAL MATCH (dec0:Decision)-[:GOVERNS]->(d) OPTIONAL MATCH p1 = (d)-[:MENTIONS]->(:Symbol)-[:CALLS*0..2]->(g1:Symbol)<-[:GOVERNS]-(dec1:Decision) OPTIONAL MATCH p2 = (d)<-[:IN_SECTION]-(:CodeExample)-[:USES]->(:Symbol)-[:CALLS*0..2]->(g2:Symbol)<-[:GOVERNS]-(dec2:Decision) WITH collect(DISTINCT {dec: dec0, via: d.heading_path}) + collect(DISTINCT {dec: dec1, via: g1.qualname}) + collect(DISTINCT {dec: dec2, via: g2.qualname}) AS hits UNWIND hits AS hit WITH hit.dec AS dec, hit.via AS via WHERE dec IS NOT NULL OPTIONAL MATCH chain = (newer:Decision {status: 'active'})-[:SUPERSEDES*1..10]->(dec) WITH coalesce(newer, dec) AS current, dec AS matched, via WHERE current.status = 'active' RETURN DISTINCT current.id AS id, current.text AS text, current.source_url AS source, current.author AS author, current.created_at AS createdAt, matched.id AS matched, via ``` **`decisionsForSymbol`** — Active decisions governing a symbol or anything it calls within 2 hops; superseded decisions resolve to their successor. ```cypher MATCH p = (s:Symbol {id: $id})-[:CALLS*0..2]->(g:Symbol)<-[:GOVERNS]-(dec:Decision) OPTIONAL MATCH chain = (newer:Decision {status: 'active'})-[:SUPERSEDES*1..10]->(dec) WITH coalesce(newer, dec) AS current, dec AS matched, g.qualname AS via WHERE current.status = 'active' RETURN DISTINCT current.id AS id, current.text AS text, current.source_url AS source, current.author AS author, current.created_at AS createdAt, matched.id AS matched, via ``` **`sectionReachedByPr`** — Guard for edits: is there a path from this pull request's changes to the section (or one of its examples)? ```cypher MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' MATCH p = (s)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-(d) WHERE d.id = $section OR (d)-[:IN_SECTION]->(:DocSection {id: $section}) WITH p ORDER BY length(p) LIMIT 1 RETURN [n IN nodes(p) | coalesce(n.qualname, n.id)] AS path ``` **`predictedCalls`** — Examples that call a symbol the pull request removed, renamed, stopped exporting or re-signed, with the call as written (checked against the new signature in code). ```cypher MATCH (pr:PullRequest {url: $pr})<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind IN ['removed', 'renamed', 'signature', 'export-removed'] MATCH (e:CodeExample)-[u:USES]->(s) WHERE e.status = 'live' AND coalesce(e.directive, '') = '' AND e.lang IN ['python', 'go', 'javascript', 'typescript', 'js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'node'] OPTIONAL MATCH (e)-[:IN_SECTION]->(sec:DocSection) RETURN e.id AS example, e.code AS code, e.lang AS lang, e.lineno AS lineno, e.url AS url, e.repo AS repo, e.path AS path, coalesce(sec.id, e.id) AS section, u.line AS line, u.text AS text, u.resolved AS resolved, s.qualname AS symbol, s.name AS name, s.kind AS symbolKind, s.decorators AS decorators, s.path AS symbolPath, c.kind AS change, c.before AS before, c.after AS after, c.renamed_to AS renamedTo ORDER BY example, line ``` **`mismatchesFor`** — Example lines that already did not fit the code when indexed (MISMATCH edges), for the given examples. ```cypher MATCH (e:CodeExample)-[m:MISMATCH]->(s:Symbol) WHERE e.id IN $ids AND e.status = 'live' RETURN e.id AS example, e.url AS url, m.line AS line, m.kind AS kind, m.detail AS detail, m.text AS call ORDER BY example, line ``` **`findSymbols`** — Symbols a name refers to: an id, a qualified name, a dotted suffix (Graph.query) or a bare name. ```cypher MATCH (s:Symbol) WHERE s.status = 'live' AND s.kind <> 'module' AND (s.id = $name OR s.qualname = $name OR s.qualname ENDS WITH $suffix OR s.name = $name) AND (size($repos) = 0 OR s.repo IN $repos) RETURN s.id AS id, s.qualname AS qualname, s.name AS name, s.kind AS kind, s.repo AS repo, s.signature AS signature, s.path AS path, s.lineno AS lineno ORDER BY qualname LIMIT 50 ``` **`docsForSymbol`** — Every doc section and example that reaches a symbol: symbol ← up to 3 callers (any repository) ← doc, with the path. Impact without a pull request. ```cypher MATCH (s:Symbol) WHERE s.id IN $ids MATCH p = (s)<-[:CALLS*0..3]-(t:Symbol)<-[:MENTIONS|USES]-(d) WHERE (d:DocSection OR d:CodeExample) AND d.status = 'live' OPTIONAL MATCH (d)-[:IN_SECTION]->(sec:DocSection) RETURN s.qualname AS changed, 'lookup' AS change, s.signature AS before, s.signature AS after, t.qualname AS reached, labels(d)[0] AS kind, d.id AS doc, d.repo AS repo, d.path AS path, coalesce(sec.id, d.id) AS section, coalesce(sec.heading_path, d.heading_path, d.path) AS heading, d.url AS url, length(p) AS hops, [n IN nodes(p) | coalesce(n.qualname, n.id)] AS evidence, [x IN relationships(p) | coalesce(x.resolved, 'exact')] AS resolution ORDER BY hops, repo, doc ``` **`impactOfChanges`** — Impact of changes that are not a pull request yet (a working tree, an editor's edits): the impact walk from a list of changed symbols. ```cypher UNWIND $changes AS ch MATCH (s:Symbol {id: ch.id}) WHERE ch.kind <> 'added' MATCH p = (s)<-[:CALLS*0..3]-(t:Symbol)<-[:MENTIONS|USES]-(d) WHERE (d:DocSection OR d:CodeExample) AND d.status = 'live' OPTIONAL MATCH (d)-[:IN_SECTION]->(sec:DocSection) RETURN s.qualname AS changed, ch.kind AS change, ch.before AS before, ch.after AS after, t.qualname AS reached, labels(d)[0] AS kind, d.id AS doc, d.repo AS repo, d.path AS path, coalesce(sec.id, d.id) AS section, coalesce(sec.heading_path, d.heading_path, d.path) AS heading, d.url AS url, length(p) AS hops, [n IN nodes(p) | coalesce(n.qualname, n.id)] AS evidence, [x IN relationships(p) | coalesce(x.resolved, 'exact')] AS resolution ORDER BY hops, repo, doc ``` **`predictedCallsOfChanges`** — Examples that call a changed symbol (removed, renamed, stopped exporting, re-signed) in changes that are not a pull request yet. ```cypher UNWIND $changes AS ch MATCH (s:Symbol {id: ch.id}) WHERE ch.kind IN ['removed', 'renamed', 'signature', 'export-removed'] MATCH (e:CodeExample)-[u:USES]->(s) WHERE e.status = 'live' AND coalesce(e.directive, '') = '' AND e.lang IN ['python', 'go', 'javascript', 'typescript', 'js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'node'] OPTIONAL MATCH (e)-[:IN_SECTION]->(sec:DocSection) RETURN e.id AS example, e.code AS code, e.lang AS lang, e.lineno AS lineno, e.url AS url, e.repo AS repo, e.path AS path, coalesce(sec.id, e.id) AS section, u.line AS line, u.text AS text, u.resolved AS resolved, s.qualname AS symbol, s.name AS name, s.kind AS symbolKind, s.decorators AS decorators, s.path AS symbolPath, ch.kind AS change, ch.before AS before, ch.after AS after, ch.renamed_to AS renamedTo ORDER BY example, line ``` **`clearanceOfChanges`** — Proof of absence for changes that are not a pull request yet: changed symbols no doc reaches within 3 call hops. ```cypher UNWIND $changes AS ch MATCH (s:Symbol {id: ch.id}) WHERE ch.kind <> 'added' OPTIONAL MATCH p = (s)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-() WITH s, ch, count(p) AS reach WHERE reach = 0 RETURN s.qualname AS symbol, s.name AS name, s.repo AS repo, ch.kind AS change, s.visibility AS visibility ORDER BY symbol ``` **`prCommits`** — The two commits a pull request is compared between: its docs are linted at both, and findings only the second has are what it makes wrong. ```cypher MATCH (pr:PullRequest {url: $url}) RETURN pr.repo AS repo, pr.before_sha AS before, pr.after_sha AS after ``` ## Memory **`cachedRun`** — An identical earlier run (same program, same commits): the graph remembers what already ran. ```cypher MATCH (r:Run {key: $key}) RETURN properties(r) AS props ORDER BY r.created_at DESC LIMIT 1 ``` **`upsertRun`** — Record an example execution: status, error line, traceback, commits installed. ```cypher MATCH (e:CodeExample {id: $example}) MERGE (r:Run {id: $id}) SET r += $props MERGE (r)-[:OF]->(e) ``` **`linkRunToPr`** — Attach a run to the pull request it was evidence for. ```cypher MATCH (r:Run {id: $id}) MATCH (pr:PullRequest {url: $pr}) MERGE (r)-[:AT]->(pr) ``` **`priorVerdicts`** — What Staleproof concluded about this section on earlier pull requests, and what maintainers did with it. ```cypher MATCH (v:Verdict)-[:ABOUT]->(:DocSection {id: $id}) MATCH (v)-[:FOR]->(pr:PullRequest) RETURN v.id AS id, v.verdict AS verdict, v.status AS status, v.reason AS reason, pr.url AS pr, pr.title AS title, v.created_at AS createdAt ORDER BY createdAt DESC LIMIT 5 ``` **`createDecision`** — Record a maintainer decision. ```cypher CREATE (dec:Decision {id: $id}) SET dec += $props RETURN dec.id AS id ``` **`governSections`** — Attach a decision to the doc sections it governs. ```cypher MATCH (dec:Decision {id: $id}) MATCH (d:DocSection) WHERE d.id IN $governs MERGE (dec)-[:GOVERNS]->(d) RETURN count(d) AS n ``` **`governSymbols`** — Attach a decision to the symbols it governs. ```cypher MATCH (dec:Decision {id: $id}) MATCH (s:Symbol) WHERE s.id IN $governs MERGE (dec)-[:GOVERNS]->(s) RETURN count(s) AS n ``` **`supersedeDecision`** — Mark an older decision superseded by a newer one. ```cypher MATCH (newer:Decision {id: $newer}) MATCH (older:Decision {id: $older}) MERGE (newer)-[:SUPERSEDES]->(older) SET older.status = 'superseded' ``` **`createVerdict`** — Record the agent's verdict on one section for one pull request, with the decisions and runs it cites. ```cypher MATCH (d:DocSection {id: $section}) MATCH (pr:PullRequest {url: $pr}) CREATE (v:Verdict {id: $id}) SET v += $props MERGE (v)-[:ABOUT]->(d) MERGE (v)-[:FOR]->(pr) WITH v OPTIONAL MATCH (dec:Decision) WHERE dec.id IN $cites FOREACH (_ IN CASE WHEN dec IS NULL THEN [] ELSE [1] END | MERGE (v)-[:CITES]->(dec)) WITH DISTINCT v OPTIONAL MATCH (r:Run) WHERE r.id IN $runs FOREACH (_ IN CASE WHEN r IS NULL THEN [] ELSE [1] END | MERGE (v)-[:EVIDENCE]->(r)) RETURN DISTINCT v.id AS id ``` **`verdictsForPr`** — The latest verdict per section for a pull request (what the docs patch is built from). ```cypher MATCH (v:Verdict)-[:FOR]->(:PullRequest {url: $pr}) MATCH (v)-[:ABOUT]->(d:DocSection) OPTIONAL MATCH (v)-[:CITES]->(dec:Decision) OPTIONAL MATCH (v)-[:EVIDENCE]->(r:Run) WITH v, d, collect(DISTINCT dec.id) AS cites, collect(DISTINCT {id: r.id, status: r.status, side: r.side, error: r.error}) AS runs ORDER BY v.created_at DESC WITH d, collect({v: v, cites: cites, runs: runs})[0] AS latest RETURN d.id AS section, d.repo AS repo, d.path AS path, d.heading_path AS heading, d.lineno AS lineno, d.end_lineno AS endLineno, d.url AS url, latest.v.id AS id, latest.v.verdict AS verdict, latest.v.status AS status, latest.v.reason AS reason, latest.v.new_text AS newText, latest.v.old_text AS oldText, latest.v.evidence AS evidence, latest.cites AS cites, latest.runs AS runs ORDER BY repo, path, lineno ``` **`reviewVerdict`** — A maintainer accepts or rejects a verdict; precision is measured from these. ```cypher MATCH (v:Verdict {id: $id}) SET v.status = $status, v.reviewed_by = $by, v.reviewed_at = $at RETURN v.id AS id ``` **`sinceLastCheck`** — What changed in the graph since a moment: pull requests checked, verdicts, decisions, runs. ```cypher OPTIONAL MATCH (pr:PullRequest) WHERE pr.ingested_at > $since WITH collect(DISTINCT {url: pr.url, title: pr.title}) AS prs OPTIONAL MATCH (v:Verdict) WHERE v.created_at > $since WITH prs, count(v) AS verdicts, sum(CASE WHEN v.verdict = 'stale' THEN 1 ELSE 0 END) AS stale OPTIONAL MATCH (dec:Decision) WHERE dec.created_at > $since WITH prs, verdicts, stale, collect(DISTINCT {id: dec.id, text: dec.text}) AS decisions OPTIONAL MATCH (r:Run) WHERE r.created_at > $since RETURN prs, verdicts, stale, decisions, count(r) AS runs, sum(CASE WHEN r.status = 'fail' THEN 1 ELSE 0 END) AS failedRuns ``` **`lastCheck`** — When the previous agent check ran (for "since your last run"). ```cypher MATCH (c:Check) WHERE c.status <> 'running' RETURN c.id AS id, c.started_at AS startedAt, c.pr AS pr ORDER BY c.started_at DESC LIMIT 1 ``` **`createCheck`** — Start an agent check of a pull request. ```cypher MATCH (pr:PullRequest {url: $pr}) CREATE (c:Check {id: $id}) SET c += $props MERGE (c)-[:FOR]->(pr) ``` **`finishCheck`** — Close an agent check: status, summary, and the full trace (every tool call with its Cypher). ```cypher MATCH (c:Check {id: $id}) SET c += $props WITH c MATCH (v:Verdict {check: $id}) MERGE (c)-[:PRODUCED]->(v) RETURN count(v) AS verdicts ``` **`sectionsWithVerdict`** — Sections that received a verdict during this check (the stop condition). ```cypher MATCH (v:Verdict {check: $id})-[:ABOUT]->(d:DocSection) RETURN DISTINCT d.id AS id ``` ## Indexing **`upsertPullRequest`** — Record a pull request and forget its previous change set (re-checking is idempotent). ```cypher MERGE (pr:PullRequest {url: $url}) SET pr += $props WITH pr OPTIONAL MATCH (pr)<-[c:CHANGED_IN]-() DELETE c ``` **`upsertMissingSymbols`** — Symbols a PR removed or renamed that the graph (indexed after the PR) no longer has: recreate them as removed. ```cypher UNWIND $rows AS row MERGE (s:Symbol {id: row.id}) ON CREATE SET s += row.props, s.status = 'removed' WITH s, row MERGE (f:File {id: row.file}) ON CREATE SET f.repo = row.props.repo, f.path = row.props.path, f.kind = 'python' MERGE (s)-[:DEFINED_IN]->(f) ``` **`lateLinkSections`** — Link docs that still name a removed symbol (by the identifiers stored on each section), within repositories that document its repository. ```cypher UNWIND $rows AS row MATCH (s:Symbol {id: row.id}) MATCH (d:DocSection {status: 'live'}) WHERE (row.name IN d.idents OR row.dotted IN d.idents) AND (d.repo = s.repo OR (:Repo {name: d.repo})-[:DEPENDS_ON]->(:Repo {name: s.repo})) MERGE (d)-[m:MENTIONS]->(s) ON CREATE SET m.resolved = 'late', m.span = row.name RETURN count(m) AS linked ``` **`lateLinkExamples`** — Link code examples that still use a removed symbol's name. ```cypher UNWIND $rows AS row MATCH (s:Symbol {id: row.id}) MATCH (e:CodeExample {status: 'live'}) WHERE row.name IN e.idents AND (e.repo = s.repo OR (:Repo {name: e.repo})-[:DEPENDS_ON]->(:Repo {name: s.repo})) MERGE (e)-[u:USES]->(s) ON CREATE SET u.resolved = 'late', u.text = row.name RETURN count(u) AS linked ``` **`upsertChangedIn`** — Attach each changed symbol to the pull request with the kind of change. ```cypher UNWIND $rows AS row MATCH (s:Symbol {id: row.id}) MATCH (pr:PullRequest {url: $url}) MERGE (s)-[c:CHANGED_IN]->(pr) SET c.kind = row.kind, c.before = row.before, c.after = row.after, c.renamed_to = row.renamed_to ``` **`upsertRepo`** — Create or refresh a repository node. ```cypher MERGE (r:Repo {name: $name}) SET r += $props ``` **`upsertFiles`** — Create or refresh files and attach them to their repository. ```cypher UNWIND $rows AS row MERGE (f:File {id: row.id}) SET f += row.props, f.seen = $run WITH f, row MATCH (r:Repo {name: row.repo}) MERGE (f)-[:IN_REPO]->(r) ``` **`upsertSymbols`** — Create or refresh symbols (modules, classes, functions, methods) in their files. ```cypher UNWIND $rows AS row MERGE (s:Symbol {id: row.id}) SET s += row.props, s.status = 'live', s.seen = $run WITH s, row MATCH (f:File {id: row.file}) MERGE (s)-[:DEFINED_IN]->(f) ``` **`deleteRepoCalls`** — Drop the outgoing call edges of a repository before re-resolving them. ```cypher MATCH (s:Symbol {repo: $repo})-[c:CALLS]->() DELETE c ``` **`upsertCalls`** — Write resolved call edges (exact = through imports and types; by-name = name match in an imported repository). ```cypher UNWIND $rows AS row MATCH (a:Symbol {id: row.from}) MATCH (b:Symbol {id: row.to}) MERGE (a)-[c:CALLS]->(b) SET c.resolved = row.resolved, c.line = row.line, c.text = row.text ``` **`markGoneSymbols`** — Symbols no longer in the repository are kept (verdicts and decisions point at them) but marked gone. ```cypher MATCH (s:Symbol {repo: $repo}) WHERE s.seen <> $run AND s.status = 'live' SET s.status = 'gone' RETURN count(s) AS gone ``` **`upsertDependsOn`** — Record that one repository depends on another (from pyproject/requirements). ```cypher UNWIND $rows AS row MATCH (a:Repo {name: row.from}) MATCH (b:Repo {name: row.to}) MERGE (a)-[d:DEPENDS_ON]->(b) SET d.spec = row.spec ``` **`upsertSections`** — Create or refresh doc sections (one per heading) with their text and identifiers. ```cypher UNWIND $rows AS row MERGE (d:DocSection {id: row.id}) SET d += row.props, d.status = 'live', d.seen = $run WITH d, row MATCH (f:File {id: row.file}) MERGE (d)-[:DEFINED_IN]->(f) ``` **`upsertExamples`** — Create or refresh code examples (fenced blocks and example scripts) and place them in their section. ```cypher UNWIND $rows AS row MERGE (e:CodeExample {id: row.id}) SET e += row.props, e.status = 'live', e.seen = $run WITH e, row MATCH (f:File {id: row.file}) MERGE (e)-[:DEFINED_IN]->(f) WITH e, row OPTIONAL MATCH (d:DocSection {id: row.section}) FOREACH (_ IN CASE WHEN d IS NULL THEN [] ELSE [1] END | MERGE (e)-[:IN_SECTION]->(d)) ``` **`deleteRepoDocEdges`** — Drop a docs repository's MENTIONS/USES/LINKS_TO/MISMATCH edges before re-linking. ```cypher MATCH (d {repo: $repo})-[r:MENTIONS|USES|LINKS_TO|MISMATCH]->() WHERE d:DocSection OR d:CodeExample DELETE r ``` **`upsertMentions`** — Link a doc section to a symbol its prose names in inline code. ```cypher UNWIND $rows AS row MATCH (d:DocSection {id: row.from}) MATCH (s:Symbol {id: row.to}) MERGE (d)-[m:MENTIONS]->(s) SET m.span = row.span, m.resolved = row.resolved ``` **`upsertUses`** — Link a code example to a symbol it calls (resolved like code: imports, then types). ```cypher UNWIND $rows AS row MATCH (e:CodeExample {id: row.from}) MATCH (s:Symbol {id: row.to}) MERGE (e)-[u:USES]->(s) SET u.line = row.line, u.resolved = row.resolved, u.text = row.text ``` **`upsertMismatches`** — Record an example line that no longer fits the code it calls: an unknown keyword, a wrong argument count, a name that does not exist. ```cypher UNWIND $rows AS row MATCH (e:CodeExample {id: row.from}) MATCH (s:Symbol {id: row.to}) MERGE (e)-[m:MISMATCH {kind: row.kind, name: row.name, detail: row.detail}]->(s) SET m.line = row.line, m.text = row.text ``` **`upsertLinks`** — Link doc sections that hyperlink to each other (input to PageRank). ```cypher UNWIND $rows AS row MATCH (a:DocSection {id: row.from}) MATCH (b:DocSection {id: row.to}) MERGE (a)-[:LINKS_TO]->(b) ``` **`resetPullRequest`** — An open pull request moved (new pushes): drop its old symbol diff so it is ingested again. ```cypher MATCH (pr:PullRequest {url: $url}) SET pr.ingested_at = null WITH pr MATCH (pr)<-[c:CHANGED_IN]-() DELETE c ``` **`markGoneDocs`** — Doc sections and examples no longer present are kept but marked gone. ```cypher MATCH (d {repo: $repo}) WHERE (d:DocSection OR d:CodeExample) AND d.seen <> $run AND d.status = 'live' SET d.status = 'gone' RETURN count(d) AS gone ``` **`deleteRepoFindings`** — Drop a repository's Finding nodes before storing the current ones (a re-index replaces them). ```cypher MATCH (f:Finding {repo: $repo}) DETACH DELETE f ``` **`findingSections`** — The live doc sections of a repository with their line ranges, to place each finding in the section that holds it. ```cypher MATCH (d:DocSection {repo: $repo}) WHERE d.status = 'live' RETURN d.id AS id, d.path AS path, d.lineno AS lineno, d.end_lineno AS endLineno ``` **`upsertFindings`** — Store what lint found in the docs and AI agent files (stale paths, links, commands, env vars), each in its doc section when it is in one. ```cypher UNWIND $rows AS row MERGE (f:Finding {id: row.id}) SET f = row.props WITH f, row OPTIONAL MATCH (d:DocSection {id: row.section}) FOREACH (_ IN CASE WHEN d IS NULL THEN [] ELSE [1] END | MERGE (f)-[:IN_SECTION]->(d)) ``` **`markLinted`** — Record whether a repository's docs references were checked at index time, and how many findings it has. ```cypher MATCH (r:Repo {name: $repo}) SET r.findings_checked = $checked, r.findings = $count ``` ## Reporting **`recentChecks`** — Agent checks, newest first. ```cypher MATCH (c:Check)-[:FOR]->(pr:PullRequest) RETURN c.id AS id, pr.url AS pr, pr.title AS title, c.status AS status, c.model AS model, c.started_at AS startedAt, c.finished_at AS finishedAt, c.steps AS steps, c.tool_calls AS toolCalls, c.verdicts AS verdicts, c.summary AS summary, c.tool_order AS toolOrder ORDER BY startedAt DESC LIMIT $limit ``` **`checkTrace`** — The stored trace of one agent check. ```cypher MATCH (c:Check {id: $id}) RETURN c.trace AS trace ``` **`auditRepos`** — The repositories a docs audit covers, with the commit each was indexed at. ```cypher MATCH (r:Repo) WHERE size($repos) = 0 OR r.name IN $repos OPTIONAL MATCH (s:Symbol {repo: r.name}) WITH r, count(s) AS symbols OPTIONAL MATCH (d:DocSection {repo: r.name}) WHERE d.status = 'live' RETURN r.name AS name, r.sha AS sha, r.indexed_at AS indexedAt, coalesce(r.index_version, 0) AS indexVersion, symbols, count(d) AS sections, coalesce(r.findings_checked, false) AS findingsChecked ORDER BY name ``` **`auditMismatches`** — Every example line that does not fit the code it calls, in the audited repositories' docs or against their code. ```cypher MATCH (e:CodeExample)-[m:MISMATCH]->(s:Symbol) WHERE e.status = 'live' AND (e.repo IN $repos OR s.repo IN $repos) OPTIONAL MATCH (e)-[:IN_SECTION]->(sec:DocSection) OPTIONAL MATCH (dr:Repo {name: e.repo}) RETURN e.id AS example, e.url AS url, e.repo AS repo, e.path AS path, coalesce(sec.heading_path, e.path) AS heading, m.line AS line, m.kind AS kind, m.detail AS detail, m.text AS call, s.qualname AS symbol, s.repo AS symbolRepo, dr.rule_settings AS ruleSettings ORDER BY repo, path, line ``` **`auditSymbols`** — Every live symbol of the audited code repositories with how many docs reference it directly and how many symbols call it (API coverage). ```cypher MATCH (s:Symbol) WHERE s.repo IN $repos AND s.status = 'live' AND s.kind <> 'module' OPTIONAL MATCH (s)<-[:MENTIONS|USES]-(d) WHERE d.status = 'live' WITH s, count(DISTINCT d) AS docs OPTIONAL MATCH (s)<-[:CALLS]-(caller:Symbol) WITH s, docs, collect(DISTINCT caller.repo) AS callerRepos, count(DISTINCT caller) AS callers RETURN s.id AS id, s.qualname AS qualname, s.name AS name, s.kind AS kind, s.repo AS repo, s.path AS path, s.lineno AS lineno, s.signature AS signature, s.visibility AS visibility, s.exported AS exported, docs, callers, callerRepos ``` **`auditExamples`** — The live examples in the audited repositories' docs, and examples elsewhere that call their code, with how many symbols each calls. ```cypher MATCH (e:CodeExample) WHERE e.status = 'live' AND e.repo IN $repos OPTIONAL MATCH (e)-[u:USES]->(:Symbol) RETURN e.id AS id, e.lang AS lang, e.url AS url, e.repo AS repo, coalesce(e.directive, '') AS directive, count(u) AS uses UNION MATCH (e:CodeExample)-[:USES]->(s:Symbol) WHERE e.status = 'live' AND s.repo IN $repos AND NOT e.repo IN $repos OPTIONAL MATCH (e)-[u:USES]->(:Symbol) RETURN e.id AS id, e.lang AS lang, e.url AS url, e.repo AS repo, coalesce(e.directive, '') AS directive, count(u) AS uses ``` **`auditRuns`** — Recorded runs of the audited examples at the indexed commits (not before/after a pull request). ```cypher MATCH (r:Run)-[:OF]->(e:CodeExample) WHERE e.id IN $ids AND r.side = 'current' RETURN e.id AS example, e.url AS url, r.status AS status, r.error AS error, r.reason AS reason, r.installed AS installed, r.created_at AS at ORDER BY at ASC ``` **`auditFindings`** — What lint found at index time in the audited repositories' docs and AI agent files (stale paths, links, commands, env vars), with the section each is in. ```cypher MATCH (f:Finding) WHERE f.repo IN $repos OPTIONAL MATCH (f)-[:IN_SECTION]->(d:DocSection) RETURN f.rule AS rule, f.severity AS severity, f.message AS message, f.repo AS repo, f.path AS path, f.line AS line, f.column AS column, f.endLine AS endLine, f.endColumn AS endColumn, f.surface AS surface, f.subject AS subject, f.evidence AS evidence, f.fix AS fix, f.fingerprint AS fingerprint, f.lowerConfidence AS lowerConfidence, d.heading_path AS heading ORDER BY repo, path, line, column ``` **`repoRuleSettings`** — The `[rules]` each repository had when it was indexed (JSON), so a check counts a beta example rule the repository opts in. ```cypher MATCH (r:Repo) WHERE r.name IN $repos RETURN r.name AS name, r.rule_settings AS ruleSettings ``` **`repoSymbols`** — List the live symbols of a repository. ```cypher MATCH (s:Symbol {repo: $repo, status: 'live'}) RETURN s.id AS id, s.qualname AS qualname, s.kind AS kind, s.signature AS signature ORDER BY s.qualname ``` **`callsFrom`** — Outgoing calls of one symbol. ```cypher MATCH (a:Symbol {id: $id})-[c:CALLS]->(b:Symbol) RETURN b.id AS to, c.resolved AS resolved, c.line AS line ORDER BY b.id ``` **`graphCounts`** — Live node counts for FACTS.md (symbols and docs a re-index marked gone, or a PR removed, are not counted). ```cypher MATCH (n) WHERE NOT (n:Symbol OR n:DocSection OR n:CodeExample) OR n.status = 'live' RETURN labels(n)[0] AS label, count(n) AS n ORDER BY label ``` **`edgeCounts`** — Edge counts by type for FACTS.md. ```cypher MATCH ()-[r]->() RETURN type(r) AS type, count(r) AS n ORDER BY type ``` **`factsRepos`** — Per-repository counts for FACTS.md. ```cypher MATCH (r:Repo) OPTIONAL MATCH (s:Symbol {repo: r.name, status: 'live'}) WITH r, count(s) AS symbols OPTIONAL MATCH (d:DocSection {repo: r.name, status: 'live'}) WITH r, symbols, count(d) AS sections OPTIONAL MATCH (e:CodeExample {repo: r.name, status: 'live'}) RETURN r.name AS name, r.sha AS sha, r.packages AS packages, symbols, sections, count(e) AS examples, sum(CASE WHEN e.lang = 'python' THEN 1 ELSE 0 END) AS pythonExamples ORDER BY name ``` **`factsCalls`** — How CALLS edges were resolved, and how many cross repositories. ```cypher MATCH (a:Symbol)-[c:CALLS]->(b:Symbol) RETURN sum(CASE WHEN c.resolved = 'exact' THEN 1 ELSE 0 END) AS exact, sum(CASE WHEN c.resolved = 'by-name' THEN 1 ELSE 0 END) AS byName, sum(CASE WHEN a.repo <> b.repo THEN 1 ELSE 0 END) AS crossRepo ``` **`factsUses`** — How USES edges (example → symbol) were resolved. ```cypher MATCH (:CodeExample)-[u:USES]->(:Symbol) RETURN sum(CASE WHEN u.resolved = 'exact' THEN 1 ELSE 0 END) AS exact, sum(CASE WHEN u.resolved = 'by-name' THEN 1 ELSE 0 END) AS byName ``` **`factsDependsOn`** — Repository dependency edges. ```cypher MATCH (a:Repo)-[d:DEPENDS_ON]->(b:Repo) RETURN a.name AS from, b.name AS to, d.spec AS spec ORDER BY from, to ``` **`factsCrossRepoDocPaths`** — Symbol → doc pairs connected through ≤ 3 call hops where the doc lives in another repository. ```cypher MATCH (s:Symbol {status: 'live'}) MATCH p = (s)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-(d) WHERE (d:DocSection OR d:CodeExample) AND s.repo <> d.repo RETURN count(DISTINCT s.id + '|' + d.id) AS n ``` **`factsSamplePaths`** — For public symbols, the longest path where a call crosses a repository boundary before reaching a doc. ```cypher MATCH (s:Symbol {status: 'live', visibility: 'public'}) WHERE NOT s.qualname CONTAINS '._' MATCH p = (s)<-[:CALLS*1..3]-(t:Symbol)<-[:MENTIONS|USES]-(d) WHERE (d:DocSection OR d:CodeExample) AND s.repo <> t.repo WITH s, d, p, [n IN nodes(p) | coalesce(n.qualname, n.id)] AS names ORDER BY length(p) DESC, d.id, reduce(k = '', x IN names | k + x + ' ') WITH s, collect({doc: d.id, hops: length(p), path: names})[0] AS best RETURN s.qualname AS symbol, best.doc AS doc, best.hops AS hops, best.path AS path ORDER BY hops DESC, symbol LIMIT 5 ``` **`factsPullRequests`** — Pull requests checked so far, with how many symbols they changed and how many doc sections those reach. ```cypher MATCH (pr:PullRequest) OPTIONAL MATCH (pr)<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' WITH pr, collect(s) AS changed OPTIONAL MATCH p = (pr)<-[c2:CHANGED_IN]-(s2:Symbol)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-(d) WHERE c2.kind <> 'added' AND (d:DocSection OR d:CodeExample) AND d.status = 'live' OPTIONAL MATCH (d)-[:IN_SECTION]->(sec:DocSection) WITH pr, changed, collect(DISTINCT coalesce(sec.id, d.id)) AS sections, collect(DISTINCT CASE WHEN d.repo <> pr.repo THEN coalesce(sec.id, d.id) END) AS elsewhere RETURN pr.url AS url, pr.title AS title, size(changed) AS changed, size(sections) AS sections, size(elsewhere) AS otherRepos ORDER BY url ``` **`factsGrepBaseline`** — Per pull request: the names of the symbols it changed and the doc sections the impact traversal reaches from them (the grep baseline in FACTS compares the two). ```cypher MATCH (pr:PullRequest)<-[c:CHANGED_IN]-(s:Symbol) WHERE c.kind <> 'added' WITH pr, collect(DISTINCT s.name) AS names OPTIONAL MATCH p = (pr)<-[c2:CHANGED_IN]-(:Symbol)<-[:CALLS*0..3]-(:Symbol)<-[:MENTIONS|USES]-(d) WHERE c2.kind <> 'added' AND (d:DocSection OR d:CodeExample) AND d.status = 'live' OPTIONAL MATCH (d)-[:IN_SECTION]->(sec:DocSection) RETURN pr.url AS url, names, collect(DISTINCT coalesce(sec.id, d.id)) AS reached ORDER BY url ``` **`factsSectionBodies`** — The text of every live doc section, for the grep baseline. ```cypher MATCH (d:DocSection) WHERE d.status = 'live' RETURN d.id AS id, d.body AS body ``` **`factsRuns`** — Every recorded example execution (the latest attempt of each identical run), latest first per example and side. ```cypher MATCH (r:Run)-[:OF]->(:CodeExample) WITH r.key AS key, max(r.created_at) AS latest MATCH (r:Run {key: key})-[:OF]->(e:CodeExample) WHERE r.created_at = latest OPTIONAL MATCH (r)-[:AT]->(pr:PullRequest) RETURN e.id AS example, e.url AS url, r.side AS side, r.status AS status, r.error AS error, r.reason AS reason, r.installed AS installed, pr.url AS pr, r.created_at AS at ORDER BY example, at DESC ``` **`impactFromSymbol`** — Docs reachable from one symbol (FACTS timing; the PR-scoped version is the impact tool). ```cypher MATCH (s:Symbol {qualname: $qualname}) MATCH p = (s)<-[:CALLS*0..3]-(t:Symbol)<-[:MENTIONS|USES]-(d) WHERE d:DocSection OR d:CodeExample RETURN d.id AS doc, length(p) AS hops ``` --- # FACTS Generated from the `falkordb` graph by `staleproof facts` at 2026-10-16T17:19:38.971Z (FalkorDB 4.22.0). Every number in the README comes from this file. Regenerate it; never edit it by hand. ## Repositories | Repository | Commit | Symbols | Doc sections | Code examples (Python) | |---|---|---:|---:|---:| | [FalkorDB/GraphRAG-SDK](https://github.com/FalkorDB/GraphRAG-SDK) | `35e67a0` | 1016 | 513 | 313 (252) | | [FalkorDB/docs](https://github.com/FalkorDB/docs) | `f804fd7` | 0 | 2618 | 1517 (165) | | [FalkorDB/falkordb-py](https://github.com/FalkorDB/falkordb-py) | `31f63f4` | 282 | 7 | 5 (2) | ## Graph | Nodes (live) | Count | |---|---:| | Check | 13 | | CodeExample | 1835 | | DocSection | 3138 | | File | 329 | | PullRequest | 4 | | Repo | 3 | | Run | 57 | | Symbol | 1298 | | Verdict | 70 | | Edges | Count | |---|---:| | ABOUT | 70 | | AT | 57 | | CALLS | 1708 | | CHANGED_IN | 9 | | DEFINED_IN | 6271 | | DEPENDS_ON | 3 | | EVIDENCE | 756 | | FOR | 83 | | IN_REPO | 329 | | IN_SECTION | 1824 | | LINKS_TO | 644 | | MENTIONS | 594 | | OF | 57 | | PRODUCED | 70 | | USES | 716 | ## Resolution quality - CALLS edges: 1708 (1655 exact through imports and types, 53 by name inside an imported repository) - CALLS edges that cross a repository boundary: 5 - USES edges from code examples: 716 (633 exact, 83 by name) - Symbol → doc paths (≤ 3 call hops) that cross a repository boundary: 1257 ## Repository dependencies - FalkorDB/GraphRAG-SDK → FalkorDB/falkordb-py (declares falkordb) - FalkorDB/docs → FalkorDB/GraphRAG-SDK (examples import graphrag_sdk) - FalkorDB/docs → FalkorDB/falkordb-py (examples import falkordb) ## Sample cross-repository paths - 4 hops: `falkordb.asyncio.falkordb.FalkorDB` ← `graphrag_sdk.core.connection.FalkorDBConnection._ensure_client` ← `graphrag_sdk.core.connection.FalkorDBConnection.query` ← `graphrag_sdk.storage.vector_store.VectorStore.backfill_entity_embeddings` ← `FalkorDB/GraphRAG-SDK:docs/api-reference.mdx#finalize` - 4 hops: `falkordb.asyncio.falkordb.FalkorDB.select_graph` ← `graphrag_sdk.core.connection.FalkorDBConnection._ensure_client` ← `graphrag_sdk.core.connection.FalkorDBConnection.query` ← `graphrag_sdk.storage.vector_store.VectorStore.backfill_entity_embeddings` ← `FalkorDB/GraphRAG-SDK:docs/api-reference.mdx#finalize` - 4 hops: `falkordb.asyncio.graph.AsyncGraph.query` ← `graphrag_sdk.core.connection.FalkorDBConnection.query` ← `graphrag_sdk.storage.graph_store.GraphStore.delete_document_chunks` ← `graphrag_sdk.api.main.GraphRAG.delete_document` ← `FalkorDB/GraphRAG-SDK:README.md#examples` ## Pull requests checked | Pull request | Changed symbols | Doc sections reached (≤ 3 hops) | …in other repositories | |---|---:|---:|---:| | [fix(ingestion): preserve document structure on update](https://github.com/FalkorDB/GraphRAG-SDK/pull/336) | 2 | 73 | 3 | | [fix: only forward accepted kwargs from Is_Cluster to redis.Redis](https://github.com/FalkorDB/falkordb-py/pull/262) | 1 | 1 | 0 | | [test: assert the client parses execution plans, and fix a plan parser bug](https://github.com/FalkorDB/falkordb-py/pull/267) | 2 | 3 | 2 | | [fix(asyncio): prevent connection pool kwargs mutation and await schema refresh](https://github.com/FalkorDB/falkordb-py/pull/276) | 4 | 43 | 41 | ## Without the graph: grep for the changed names For each pull request, a whole-word search of every live doc section for the names of the symbols it changed (what a reviewer can do without the graph), next to the sections the impact traversal reaches. | Pull request | Changed names | Reached by the graph | …that grep also finds | grep matches outside them | |---|---|---:|---:|---:| | FalkorDB/GraphRAG-SDK/pull/336 | `update`, `run` | 73 | 30 | 219 | | FalkorDB/falkordb-py/pull/262 | `Is_Cluster` | 1 | 0 | 0 | | FalkorDB/falkordb-py/pull/267 | `_operation_tree`, `Is_Cluster` | 3 | 0 | 0 | | FalkorDB/falkordb-py/pull/276 | `Cluster_Conn`, `_query`, `__init__` | 43 | 0 | 8 | Total: the graph reaches 120 sections; grep finds 30 of them and matches 227 others. ## Example runs (sandbox) 51 recorded runs: 5 skipped, 6 error, 24 pass, 16 fail. | Example | Side | Status | Error / reason | Installed | |---|---|---|---|---| | [FalkorDB/GraphRAG-SDK:docs/api-reference.mdx:52](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/api-reference.mdx#L716) | before of FalkorDB/GraphRAG-SDK/pull/336 | skipped | reads a local file the docs do not provide (employees.csv) | FalkorDB/GraphRAG-SDK@d703d4a, FalkorDB/falkordb-py@31f63f4 | | [FalkorDB/GraphRAG-SDK:docs/graph-schema.mdx:7](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/graph-schema.mdx#L267) | after of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@2a970dc | | [FalkorDB/GraphRAG-SDK:docs/graph-schema.mdx:7](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/graph-schema.mdx#L267) | before of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@e307df3 | | [FalkorDB/GraphRAG-SDK:docs/graph-schema.mdx:8](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/graph-schema.mdx#L276) | after of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@2a970dc | | [FalkorDB/GraphRAG-SDK:docs/graph-schema.mdx:8](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/graph-schema.mdx#L276) | before of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@e307df3 | | [FalkorDB/GraphRAG-SDK:docs/storage.mdx:16](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/storage.mdx#L314) | after of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@2a970dc | | [FalkorDB/GraphRAG-SDK:docs/storage.mdx:16](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/docs/storage.mdx#L314) | before of FalkorDB/falkordb-py/pull/276 | error | dependency conflict: graphrag-sdk==1.4.0 requires falkordb[hiredis]>=1.8.0,<2, which the other checkouts don't satisfy ( | FalkorDB/GraphRAG-SDK@35e67a0, FalkorDB/falkordb-py@e307df3 | | [FalkorDB/GraphRAG-SDK:graphrag_sdk/examples/07_incremental_updates.py:0](https://github.com/FalkorDB/GraphRAG-SDK/blob/35e67a0b27fdcc7f2c28508b09e86169851a8187/graphrag_sdk/examples/07_incremental_updates.py#L1) | before of FalkorDB/GraphRAG-SDK/pull/336 | skipped | needs an LLM provider (OPENAI_API_KEY); Staleproof does not hold provider keys | FalkorDB/GraphRAG-SDK@d703d4a, FalkorDB/falkordb-py@31f63f4 | | [FalkorDB/docs:agentic-memory/graphiti-mcp-server.mdx:19](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/agentic-memory/graphiti-mcp-server.mdx#L384) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:agentic-memory/graphiti-mcp-server.mdx:19](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/agentic-memory/graphiti-mcp-server.mdx#L384) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.config-get.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.config-get.mdx#L16) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.config-get.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.config-get.mdx#L16) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.config-set.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.config-set.mdx#L24) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.config-set.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.config-set.mdx#L24) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.constraint-create.mdx:3](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.constraint-create.mdx#L119) | after of FalkorDB/falkordb-py/pull/276 | fail | AttributeError: 'FalkorDB' object has no attribute 'create_constraint' | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.constraint-create.mdx:3](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.constraint-create.mdx#L119) | before of FalkorDB/falkordb-py/pull/276 | fail | AttributeError: 'FalkorDB' object has no attribute 'create_constraint' | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.constraint-drop.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.constraint-drop.mdx#L54) | after of FalkorDB/falkordb-py/pull/276 | fail | AttributeError: 'FalkorDB' object has no attribute 'drop_constraint' | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.constraint-drop.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.constraint-drop.mdx#L54) | before of FalkorDB/falkordb-py/pull/276 | fail | AttributeError: 'FalkorDB' object has no attribute 'drop_constraint' | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.explain.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.explain.mdx#L17) | after of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.explain.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.explain.mdx#L17) | before of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.explain.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.explain.mdx#L17) | after of FalkorDB/falkordb-py/pull/267 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@a9c7510 | | [FalkorDB/docs:commands/graph.explain.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.explain.mdx#L17) | before of FalkorDB/falkordb-py/pull/267 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@031eb04 | | [FalkorDB/docs:commands/graph.list.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.list.mdx#L14) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.list.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.list.mdx#L14) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.profile.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.profile.mdx#L22) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.profile.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.profile.mdx#L22) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:commands/graph.profile.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.profile.mdx#L22) | after of FalkorDB/falkordb-py/pull/267 | pass | | FalkorDB/falkordb-py@a9c7510 | | [FalkorDB/docs:commands/graph.profile.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.profile.mdx#L22) | before of FalkorDB/falkordb-py/pull/267 | pass | | FalkorDB/falkordb-py@031eb04 | | [FalkorDB/docs:commands/graph.slowlog.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.slowlog.mdx#L26) | after of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:commands/graph.slowlog.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/commands/graph.slowlog.mdx#L26) | before of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:genai-tools/graphrag-sdk.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/genai-tools/graphrag-sdk.mdx#L25) | before of FalkorDB/GraphRAG-SDK/pull/336 | skipped | needs an LLM provider (LiteLLM(); Staleproof does not hold provider keys | FalkorDB/GraphRAG-SDK@d703d4a, FalkorDB/falkordb-py@31f63f4 | | [FalkorDB/docs:getting-started/index.mdx:6](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/getting-started/index.mdx#L121) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:getting-started/index.mdx:6](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/getting-started/index.mdx#L121) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:index.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/index.mdx#L88) | after of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:index.mdx:1](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/index.mdx#L88) | before of FalkorDB/falkordb-py/pull/276 | fail | redis.exceptions.ResponseError: Invalid graph operation on empty key | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:operations/opentelemetry.mdx:10](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/operations/opentelemetry.mdx#L206) | after of FalkorDB/falkordb-py/pull/276 | fail | ModuleNotFoundError: No module named 'opentelemetry' | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:operations/opentelemetry.mdx:10](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/operations/opentelemetry.mdx#L206) | before of FalkorDB/falkordb-py/pull/276 | fail | ModuleNotFoundError: No module named 'opentelemetry' | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:operations/opentelemetry.mdx:4](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/operations/opentelemetry.mdx#L81) | before of FalkorDB/falkordb-py/pull/276 | skipped | contains placeholders to fill in (e.g. "") | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:udfs/index.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/udfs/index.mdx#L20) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:udfs/index.mdx:0](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/udfs/index.mdx#L20) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/docs:udfs/index.mdx:6](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/udfs/index.mdx#L158) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/docs:udfs/index.mdx:6](https://github.com/FalkorDB/docs/blob/f804fd70f915fbd6f2651b56088739c0227c36fc/udfs/index.mdx#L158) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/falkordb-py:README.md:2](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L32) | before of FalkorDB/falkordb-py/pull/276 | skipped | not Python (bash) | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/falkordb-py:README.md:3](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L39) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/falkordb-py:README.md:3](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L39) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | after of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@2a970dc | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | before of FalkorDB/falkordb-py/pull/276 | pass | | FalkorDB/falkordb-py@e307df3 | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | after of FalkorDB/falkordb-py/pull/267 | pass | | FalkorDB/falkordb-py@a9c7510 | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | before of FalkorDB/falkordb-py/pull/267 | fail | TypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry' | FalkorDB/falkordb-py@031eb04 | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | after of FalkorDB/falkordb-py/pull/262 | pass | | FalkorDB/falkordb-py@f7c3de7 | | [FalkorDB/falkordb-py:README.md:4](https://github.com/FalkorDB/falkordb-py/blob/31f63f44a5745b9bec65969a1db706906718cb82/README.md#L67) | before of FalkorDB/falkordb-py/pull/262 | fail | TypeError: Redis.__init__() got an unexpected keyword argument 'himport_registry' | FalkorDB/falkordb-py@49aa012 | ## Impact query timing (server-side, FalkorDB) | Changed symbol | Rows | ms | |---|---:|---:| | `falkordb.asyncio.falkordb.FalkorDB` | 49 | 0.74 | | `falkordb.asyncio.falkordb.FalkorDB.select_graph` | 49 | 0.79 | | `falkordb.asyncio.graph.AsyncGraph.query` | 670 | 2.85 | --- # Architecture This page is for contributors: where each part of the code lives, what happens during a check from end to end, the design decisions worth knowing before changing anything, how the documentation is published from `docs/`, and how the test suite is organised. ## Source layout ```text src/ cli.ts index | check | docs-for | audit | run | decide | verdict | patch | mcp | serve | facts | db index.ts the library's exports config.ts environment → Config audit.ts the docs audit: mismatches, API coverage, runs at the indexed commits, the score (audit_docs) pkg.ts package root and version graph/client.ts FalkorDB connection; one graph per org; every query can be traced graph/queries.ts every Cypher statement, named (the README's Cypher section and GRAPH-MODEL.md are generated from it) graph/schema.ts the exact-match indexes created before every index run indexer/ repo.ts (git checkout with the token in git's environment config, file classification, module names), tarball.ts (download a GitHub tarball and extract what the indexer reads, commit from the pax header; for hosts without git), ensure.ts (clone or download; index on first use), files.ts, treesitter.ts and python.ts (tree-sitter Python extractor), typescript.ts and tsmodules.ts (TypeScript/JavaScript into the same module shape; npm packages as js- modules), resolve.ts (org-wide symbol table, call resolver, static checks of examples, exported API), signature.ts (argument checks against a signature), markdown.ts (Markdown/MDX scanner, example directives), rst.ts (reStructuredText scanner), repo-config.ts (.staleproof.toml), docs.ts (MENTIONS/USES/LINKS_TO/MISMATCH), deps.ts (DEPENDS_ON), write.ts, index.ts (indexOrg) hosted/workspaces.ts hosted mode: a workspace graph per repository owner (gh-) indexed on demand from tarballs; the registry graph, cross-instance locks, size limits, least-recently-used eviction and daily budgets pr/ github.ts (PR metadata), changes.ts (symbol-level diff between the PR's commits), github-diff.ts (the same diff through the GitHub API when there is no git), ingest.ts (and re-reading an open PR that moved), open-pr.ts (the docs PR in a fork, upstream or the docs repository itself), predict.ts (predicted breaks), local.ts (a working tree or an editor's files as changes; check_changes) tools/ graph-tools.ts (impact, clearance, rank_sections, read_section), run-example.ts, docs-for.ts (docs_for_symbol), memory-tools.ts (decisions, verdicts, review, "since the last check"), types.ts (ToolResult, Tracer, unknown-parameter check), index.ts (tool sets, JSON schemas) runner/ index.ts (runExample, Run nodes, one run at a time), prepare.ts (skip rules, fragment stitching), sandbox.ts (venv per repo@sha set built with an allow-listed environment, the sandboxed process), scratch.ts (scratch FalkorDB start and flush) agent/ loop.ts (runAgentCheck), prompt.ts, model.ts (ModelClient), anthropic.ts (Claude and Anthropic-compatible providers), scripted.ts (ScriptedModel), graph-check.ts (graph mode), trace.ts (trace events) mcp/server.ts MCP server over the tools, plus check_pull_request, check_changes, docs_for_symbol, audit_docs and docs_patch; stdio transport (indexes a pull request's repository on first use) mcp/http.ts MCP over Streamable HTTP, stateless; in hosted mode routed to the graph of the repository each call names, without run_example server/app.ts HTTP API (+ SSE), hosted routes (/api/index, /api/repos, /api/workspaces), /api/audit, /api/docs-for, /api/badge (server/badge.ts), /api/mcp, the static web UI and the docs as plain text patch.ts verdicts → unified diff + PR description with evidence facts.ts FACTS.md from the graph format/ text.ts (terminal output), markdown.ts (the PR comment's Markdown), audit.ts (the audit as text and Markdown), report.ts (the report from the graph: impact, clearance, ranking, verdicts and the example runs before and after; what the Action posts and check_pull_request returns) web/ index.html, vite.config.ts Vite + React; the build also emits the docs as plain text (llms.ts) llms.ts /llms.txt, /llms-full.txt, /docs/.md, robots.txt, sitemap.xml src/main.tsx, App.tsx entry and routes (home, a stored check, the query catalogue, docs) src/route.ts hash routes: #/, #/, #/try?pr=, #/check/, #/audit?repo=, #/install, #/model, #/docs// src/layout.tsx header, footer, icons, code blocks src/Landing.tsx the home page's content (also prerendered) src/TryIt.tsx the live demo: paste a PR, watch the check stream in, recent checks src/Audit.tsx the docs audit page: score, mismatches, coverage, the badge src/Install.tsx, install-links.ts the install page: the prefilled workflow link, MCP buttons and commands src/CheckResults.tsx a check's findings: changed symbols, sections, runs, verdicts; a stored check replayed src/components.tsx evidence paths, Cypher lists, the trace, runs, diffs, the decision form src/state.ts trace events → view (the same reducer live and replayed) src/api.ts API client and the snapshot fallback src/Docs.tsx the docs pages: navigation, table of contents, search src/docs/manifest.ts the page list (file, slug, title, summary, group) and llms.txt builders src/docs/render.ts Markdown → HTML with GitHub-style anchors and in-app links src/prerender.tsx the home page as static HTML src/format.ts, types.ts, styles.css public/snapshot.json what the API returned at one moment, for hosts without FalkorDB python/sitecustomize.py the sandbox shim (socket and DNS) python/run_example.py the sandbox entry point: runs example.py, on an event loop when it has top-level await grammars/ tree-sitter-python.wasm scripts/ falkordb-local.sh (FalkorDB without Docker), seed.sh (index + graph-mode checks + facts), gen-docs.ts (generated docs), snapshot.ts (web/public/snapshot.json), prerender.ts (the home page into web/dist/index.html) api/index.js the Vercel Function wrapping server/app.ts (vercel.json rewrites /api/* to it) action.yml composite GitHub Action at the repository root (uses: rajkaria/staleproof@v1) examples/staleproof.yml workflow using the Action test/ vitest suite and the fixture organisation (test/fixtures/org) ``` ## A check, end to end Before the check, the repository must be in a graph. `ensureIndexed` (CLI `check`, stdio MCP) indexes the pull request's repository and any `--with` repositories when the graph lacks one, cloning with git or downloading tarballs where there is none (`checkoutOrDownload`). In hosted mode `Workspaces.ensure` finds the graph that holds the repository, or indexes it into the owner's workspace under a lock, making room by evicting the least recently used workspaces first. Then: 1. `ingestPullRequest`: PR metadata from GitHub → commits before/after → `diffRange` parses both versions of each changed Python file and compares definitions → `PullRequest` + `CHANGED_IN`; removed symbols recreated and late-linked to docs through the identifiers stored on each section. Without `git`, the same diff is read through the GitHub API. 2. `runAgentCheck` creates a `Check` node and gives the model the system prompt, the PR URL and a "since the last check" summary from the graph. Each turn streams; tool inputs are checked for unknown parameters and validated (zod) before running; tool errors go back to the model as error results. 3. Tools read and write the graph through a traced view, so every Cypher statement and its server time lands in the trace. `run_example` builds (or reuses) a virtualenv per set of repo@sha, flushes the scratch FalkorDB, and runs the example with the shim through `python/run_example.py`; identical runs are answered from `Run` nodes. 4. After any `impact` call, the loop collects the section ids of every impact page itself. When the model stops, the loop checks that each of those sections has a verdict from this check; if not, it sends up to two reminders, then records the check as incomplete. The trace is stored on the `Check` node, linked to the PR and the verdicts it produced. Graph mode (`runGraphCheck`) runs impact, clearance, rank_sections and up to 3 example runs in that order instead, with no model and no verdicts. `checkReport` then renders the report from what the graph holds: the comment the GitHub Action posts and what `check_pull_request` returns. 5. `buildDocsPatch` applies the latest stale verdicts to each file at the indexed commit, read from the local clone or from raw.githubusercontent.com (refusing sections whose text changed since), and renders a unified diff and a PR description with the evidence per section. The details: [The agent](agent.md), [Example runner](example-runner.md), [Decisions and memory](memory.md). ## Decisions worth knowing - One graph per organisation (graph name = org slug): FalkorDB's multi-graph is the tenancy boundary. - Nothing is deleted on re-index: symbols and sections are marked `gone`, so decisions and verdicts never dangle. - By-name resolution is allowed only inside repositories the module imports, never for generic names, and is narrowed by what the same docs page uses exactly; every by-name edge is labelled. - Every variable-length pattern binds a path variable and clearance counts bound paths, because an anonymous variable-length pattern missed callers on FalkorDB 4.18; [test/traversal.test.ts](../test/traversal.test.ts) keeps it that way. - The agent never edits without a path: `propose_edit` checks the graph before writing a verdict. - Hosted mode drops and re-indexes only graphs named with its `gh-` prefix; the curated graph is never touched, and a workspace is always indexed whole, so its cross-repository edges stay resolved. - Unknown tool parameters are an error, in the agent loop and over MCP, so a model never gets the same answer to an argument that was silently dropped. - Code from a pull request never sees the server's environment: the install step (which runs build code) gets an allow-list of paths, locale, proxies, certificates and `UV_*`/`PIP_*` settings; the example gets nothing inherited. - Example runs are serialised in one process (the scratch server is flushed per run), and the server runs one check at a time. - Agent mode fails loudly without credentials; graph mode is a separate, labelled mode, not a silent fallback. ## How the docs are published The documentation is the Markdown in `docs/` plus the repository README. One list, `DOCS` in [web/src/docs/manifest.ts](../web/src/docs/manifest.ts), names every page (file, slug, title, one-sentence summary, group) and drives every place they are published: | Output | Built by | Served at | |---|---|---| | Web docs pages | `web/src/Docs.tsx` loads `docs/.md` and renders it with `web/src/docs/render.ts` (GitHub-style anchors; links to other pages become in-app routes, links to code go to GitHub) | `#/docs/` | | `/llms.txt` | `llmsTxt()` in the manifest: a summary, quick facts, and one line per page | the site root | | `/llms-full.txt` | `llmsFullTxt()`: every page in reading order, each under a header | the site root | | `/docs/.md` | each page's Markdown as is | the site | | `robots.txt`, `sitemap.xml` | `web/llms.ts` | the site root | | `llms.txt` in the repository | `npm run docs`, linking to `docs/` | GitHub | `web/llms.ts` produces the plain-text files; the Vite build emits them into `web/dist`, the Vite dev server serves them, and `staleproof serve` serves them from the built web directory. On Vercel they are static files. Some parts are generated from the code and never edited by hand. `npm run docs` ([scripts/gen-docs.ts](../scripts/gen-docs.ts)) rewrites the README's Cypher section, [docs/GRAPH-MODEL.md](GRAPH-MODEL.md), the parameter reference between the generated markers in [docs/tools.md](tools.md), and the repository `llms.txt`. `staleproof facts` writes [docs/FACTS.md](FACTS.md) from the graph. Tests keep them in sync: [test/docs.test.ts](../test/docs.test.ts) fails if the README's Cypher section or GRAPH-MODEL.md drift from `src/graph/queries.ts`, and [test/docs-site.test.ts](../test/docs-site.test.ts) fails if the tools reference or `llms.txt` drift, if a page in `docs/` is missing from the manifest (or the reverse), if a page lacks its single H1 and opening paragraph, or if any relative link or heading anchor in `docs/*.md` or the README is broken. To add a page: write `docs/.md` with one H1 and a summary paragraph, add an entry to `DOCS`, then run `npm run docs` and `npm run verify`. ## Testing ```bash npm install npm run verify # typecheck (engine + web), then the whole vitest suite ``` The integration tests need a FalkorDB. Without `FALKORDB_TEST_URL`, the global setup ([test/global-setup.ts](../test/global-setup.ts)) starts the Docker-free local server on port 6380 with `scripts/falkordb-local.sh` and leaves it running for the next run. Test files run one at a time, against a fixture organisation of three small repositories (`test/fixtures/org`: a client, an SDK built on it, and a docs site). No test calls a paid model API, and GitHub API responses are faked. | Area | Test files | |---|---| | Indexing | `python.test.ts` (extractor), `resolve.test.ts` (module names, symbol table, exact and by-name resolution), `markdown.test.ts` (scanner, identifiers, links), `index-code.test.ts`, `index-org.test.ts` (the whole organisation in FalkorDB) | | Pull requests | `changes.test.ts` (change kinds, diff parsing, the GitHub API source) | | Graph queries and tools | `tools.test.ts` (impact paging, clearance, ranking, read_section), `traversal.test.ts` (variable-length paths, bound path variables) | | Example runner | `prepare.test.ts` (skip rules, stitching, outcomes, install conflicts, the install environment allow-list), `sandbox.test.ts` (the shim, timeout, no inherited secrets), `run-example.test.ts` (before/after runs in FalkorDB) | | Memory and patch | `memory.test.ts` (decisions, supersession, edit guard, verdict history, the docs patch, opening the docs PR, "since the last check") | | Agent | `agent.test.ts` (the loop with a scripted model, reminders, errors, time budget, unknown parameters, every impact page), `anthropic-model.test.ts` (the Claude and compatible request shapes against a local fake API) | | Interfaces | `mcp.test.ts`, `server.test.ts` (HTTP API and SSE), `format.test.ts` (the PR comment), `action.test.ts` (the Action calls real commands and flags), `web-state.test.ts` (the web view reducer) | | Hosted mode | `hosted.test.ts` (tarball download and the pax header, translated docs, workspaces on first use, limits and daily budgets, hosted checks and repository status, MCP over HTTP and its routing) | | Package and facts | `package.test.ts` (shipped assets, the built UI and the plain-text docs served), `facts.test.ts` | | Docs | `docs.test.ts` (generated sections, no internal material in tracked files), `docs-site.test.ts` (manifest, page shape, links and anchors, generated reference, llms.txt) | ## Related - [Core concepts](concepts.md): the vocabulary the code uses - [The agent](agent.md): the check loop in detail - [Graph model and Cypher](GRAPH-MODEL.md): every query - [Deploy](DEPLOY.md): running it on Vercel, Docker Compose or one machine --- # Deploy A deployment is one Node.js process (`staleproof serve`: the web app, the HTTP API and MCP over HTTP) plus FalkorDB. This page covers four ways to run it (Vercel with FalkorDB Cloud, Docker Compose on any VM, a plain host without Docker, and FalkorDB Cloud as the database for any of them), hosted mode and its limits (how staleproof.dev indexes any public repository on demand), connecting Claude Code over MCP, the GitHub Action, and a checklist for running it for a team. It is for whoever operates the service; every variable is described in [Configuration](configuration.md). ## Choose a host | | Vercel | Docker Compose, a VM or your machine | |---|---|---| | Web app, API and MCP at `/api/mcp` | yes | yes | | Hosted mode (any public repository on demand) | on by default (`STALEPROOF_HOSTED=off` turns it off) | with `serve --hosted` or `STALEPROOF_HOSTED=on` | | Agent mode and graph mode | yes; agent checks stop at 240 s by default | yes; no time limit unless you set one | | Indexing | tarball downloads (no git), in hosted mode | git for `staleproof index`; tarball downloads in hosted mode | | Pull request ingestion | through the GitHub API (no git) | git | | Example runs | not executed; runs recorded on the graph by another host are answered | yes, in the sandbox | | Docs patch | yes, files read from GitHub at the indexed commit | yes, from the local clones | | Raw text-match counts in `clearance` | 0 (no clones to search) | yes | The usual split: index and seed from a machine that can run examples, and serve the demo from Vercel against the same FalkorDB. ## The minimum you need | Variable | Why | |---|---| | `FALKORDB_URL` | The FalkorDB holding the org graph. Default `falkor://localhost:6379`; `serve` starts a local server there when it is unset and nothing answers | | `STALEPROOF_ORG` | The graph the server answers from (the curated graph in hosted mode), if it is not `falkordb` | | `GITHUB_TOKEN` (or `GH_TOKEN`) | Without it GitHub allows 60 API requests per hour; private repositories need it; required for `patch --open-pr` | | `ANTHROPIC_API_KEY` | Agent mode. For an Anthropic-compatible provider such as DeepSeek, also `ANTHROPIC_BASE_URL` and `STALEPROOF_MODEL` (for example `deepseek-v4-pro`). Without a key the server runs graph mode and says so | The full list, with defaults and the commands that read each one, is in [Configuration](configuration.md#environment-variables). A host that runs examples also needs `git`, `tar`, and `uv` or `python3`. With [uv](https://docs.astral.sh/uv/) each virtualenv is created for Python 3.12; without it the runner falls back to `python3 -m venv` and pip, and the examples run on whatever version `python3` is. uv is recommended. Hosts without git (serverless) skip example runs with a reason and answer recorded runs from the graph. ## Vercel The repository deploys to Vercel as is ([vercel.json](../vercel.json)): the web app is static (`web/dist`) and the API, MCP included, runs as one Vercel Function ([api/index.js](../api/index.js)); a rewrite in vercel.json sends every `/api/...` path to it. On Vercel, pull requests are read through the GitHub API (no `git`), repositories are downloaded as tarballs, `STALEPROOF_HOME` is `/tmp/staleproof`, and examples are not executed (no sandbox runtime): runs recorded by a local or container host are answered from the graph. The function runs in [hosted mode](#hosted-mode) unless `STALEPROOF_HOSTED=off`. 1. Create a FalkorDB the function can reach ([FalkorDB Cloud](#falkordb-cloud)'s free tier works) and index into it from your machine: ```bash export FALKORDB_URL='falkor://falkordb:@:' # falkors:// if the endpoint has TLS npx -y staleproof index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs bash scripts/seed.sh # from a clone of the repository: re-index, graph-mode checks of the preloaded PRs with example runs, FACTS.md ``` `scripts/seed.sh` is not in the npm package; it uses `staleproof` from your `PATH`, else the clone's `dist/cli.js`. Its example runs execute on your machine and are stored on the graph, which is what lets the Vercel deployment show them. 2. Add the variables and redeploy: ```bash vercel env add FALKORDB_URL production vercel env add GITHUB_TOKEN production # recommended: fine-grained, public repositories, read-only vercel env add ANTHROPIC_API_KEY production # optional: agent mode vercel env add ANTHROPIC_BASE_URL production # with STALEPROOF_MODEL, only for an Anthropic-compatible provider vercel env add STALEPROOF_MODEL production ``` `STALEPROOF_PRELOADED` (the pull requests on the home page), `STALEPROOF_CHECK_BUDGET_MS`, `STALEPROOF_AGENT_CHECKS_PER_DAY` (default 50) and `STALEPROOF_HOSTED` are optional. `PORT`, `HOST` and the scratch variables are not used there. Without a reachable FalkorDB the function answers `503 {"error": "the Staleproof API is not connected to FalkorDB (); set FALKORDB_URL", "offline": true}` and the web app shows the snapshot bundled at build time, labelled with the time it was taken ([snapshot mode](http-api.md#snapshot-mode); `npm run snapshot` regenerates `web/public/snapshot.json` from a graph). Function duration is capped at 300 s (`maxDuration`). An agent check in the server starts no model turn after 240 s (`STALEPROOF_CHECK_BUDGET_MS`) and ends `incomplete`, listing the sections still open; a 43-section agent check takes about 6 minutes, so run large checks from the CLI, which has no time limit. The function runs in `bom1` (Mumbai), next to the FalkorDB Cloud instance in AWS `ap-south-1`: a check makes dozens of graph round trips, so set `regions` in `vercel.json` to the Vercel region closest to your FalkorDB. ## Hosted mode Hosted mode is how staleproof.dev checks pull requests in repositories nobody indexed for it. It is on by default in the Vercel function, and `staleproof serve --hosted` (or `STALEPROOF_HOSTED=on`) turns it on anywhere else. The code is [src/hosted/workspaces.ts](../src/hosted/workspaces.ts). - **The curated graph** is the server's own (`STALEPROOF_ORG`, else `falkordb`), indexed and seeded from the CLI as before. A repository it holds answers from it; hosted mode never re-indexes, evicts or adds to it. - **Workspaces.** Any other public repository is indexed on demand into the graph of its owner, `gh-` (the owner slugged), when a check, an audit (`POST /api/audit`, the `#/audit` page), `POST /api/index` or an MCP call first names it. The repositories named with it (`with`: a docs site, an SDK) join the same workspace, and every repository of a workspace is indexed together on every refresh, so calls and doc links across them resolve. - **Downloads.** Each repository is downloaded as a tarball at its default branch head and only the files the indexer reads are extracted ([Indexing](indexing.md#without-git-tarballs)); translated docs directories, such as `docs/zh/` or `i18n/pt-BR/`, are skipped. The extracted files are deleted once the repository is indexed. - **Registry.** The graph `staleproof-hosted` records which workspace holds each repository, its commit, when it was indexed and last used, a lock per workspace (one indexing run at a time across every server instance; others wait up to 150 seconds), and the daily counters. - **No examples.** Graph-mode checks in a workspace run no examples, and the MCP endpoint does not offer `run_example`. A server with a sandbox still runs them for the curated graph. ### Limits `DEFAULT_HOSTED_LIMITS`: | Limit | Value | What happens | |---|---|---| | Repositories per workspace | 4 | Indexing a fifth is refused, with the `npx -y staleproof index …` command to run it yourself | | Workspace graph size | 20 MB | A workspace bigger than this after indexing is dropped and refused (needs `GRAPH.MEMORY USAGE` on the server; without it, the size is not checked) | | Eviction | above 70 MB, down to 55 MB | Before indexing, when the FalkorDB server uses more than 70 MB, the least recently used workspaces are dropped until it uses less than 55 MB. If it still uses more than 70 MB, indexing is refused: `the hosted service is full right now.` A dropped workspace is indexed again the next time someone asks for it | | Re-index | after 7 days | A workspace indexed more than 7 days ago, or by an older Staleproof (before index format 3: no static checks or export flags), is indexed again at its default branch heads on its next use | | Index runs | 500 per day | Across the service, per UTC day | | Archive size | 150 MB | Compressed download, checked against `Content-Length` and while streaming | | Files | 3000 | Files the indexer would read (Python, TypeScript and JavaScript, Markdown, MDX and reStructuredText, dependency manifests) | | Agent checks | 50 per day | `STALEPROOF_AGENT_CHECKS_PER_DAY` in the Vercel function; over it, agent checks answer 429 and graph mode still works | The eviction thresholds fit FalkorDB Cloud's free tier, which is 100 MB. `GET /api/health` reports the server's memory and the limits, and `GET /api/workspaces` lists every workspace ([HTTP API](http-api.md#get-apiworkspaces)). ### A GitHub token for hosted mode Give the server a `GITHUB_TOKEN`. Unauthenticated GitHub API requests are limited to 60 an hour per IP address, and Vercel's egress IP addresses are shared with other deployments, so without a token pull request ingestion (up to five API requests per new pull request) runs into other people's limit. Use a fine-grained token with access to public repositories only and no write permission. Hosted mode downloads repositories without it (from codeload), so it indexes public repositories only whatever the token can read; the token only raises the API limit for reading pull requests. ## Docker Compose (any VM) ```bash docker compose up -d --build docker compose exec staleproof node dist/cli.js index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs docker compose exec -e FACTS_OUT=/data/FACTS.md staleproof bash scripts/seed.sh # optional: index + graph-mode checks of the preloaded PRs ``` The compose file ([docker-compose.yml](../docker-compose.yml)) runs two FalkorDB containers: `falkordb` (the graph, persisted in the `falkordb-data` volume) and `scratch` (examples only, no volume, flushed before every run). The `staleproof` container serves on port 8080 with `FALKORDB_URL=falkor://falkordb:6379` and `STALEPROOF_SCRATCH_URL=falkor://scratch:6379`. - **Image**: Node 22 on Debian bookworm, with `git`, `python3`, `tar` and uv 0.11.6. `STALEPROOF_HOME` is `/data` (the `staleproof-data` volume, which keeps clones and virtualenvs across restarts), uv keeps Python and its cache under `/data`, and a health check calls `/api/health` every 30 s. - **Variables passed through** from your shell or `.env`: `STALEPROOF_ORG` (default `falkordb`), `ANTHROPIC_API_KEY`, `GITHUB_TOKEN` and `STALEPROOF_PRELOADED`. Compose passes each one as an empty string when it is unset. For `STALEPROOF_PRELOADED` that means the home page lists no pull requests, so set it explicitly if you want the preloaded list. `ANTHROPIC_BASE_URL`, `STALEPROOF_MODEL` and `GH_TOKEN` are not passed through: to use an Anthropic-compatible provider, add them to the service's `environment` in the compose file. - **`FACTS_OUT`**: `scripts/seed.sh` ends by writing FACTS.md to `docs/FACTS.md` by default, and the image has no `docs/` directory; the command above writes it to the volume instead. - **Exposure**: the API has no authentication. Put a TLS-terminating proxy in front of port 8080 for a public demo, and see the [production checklist](#production-checklist). > The Dockerfile and compose file are written but were not built on the machine Staleproof was developed on (no Docker there). Everything else in this document was run. ## Without Docker ```bash npx -y staleproof db start # FalkorDB on :6379 from the falkordblite wheel (uv fetches Python 3.12) npx -y staleproof index FalkorDB/falkordb-py FalkorDB/GraphRAG-SDK FalkorDB/docs npx -y staleproof serve --host 0.0.0.0 --port 8080 ``` `db start` installs the `falkordblite` wheel once into `~/.staleproof/falkordb/venv` (with uv, else `python3 -m venv` and pip; the wheel needs Python 3.12 or later) and keeps the data in `~/.staleproof/falkordb/data-6379`; without it, `serve` starts the same server itself. Add `--hosted` to `serve` to index any public repository on demand. The scratch server for example runs starts by itself on port 6390 the first time an example runs. `--host 0.0.0.0` exposes the unauthenticated API on every interface; keep the default `127.0.0.1` unless a proxy sits in front. ## FalkorDB Cloud Create a free instance; its Connectivity tab gives the public endpoint and Instance Details the password for user `falkordb`. The free tier's endpoint is plain TCP: `FALKORDB_URL=falkor://falkordb:@:`. URL-encode the password (`@` becomes `%40`, `:` becomes `%3A`, and so on), and use `falkors://` for an endpoint with TLS ([URL forms](configuration.md#graph-and-organisation)). Keep the scratch server separate and disposable: leave `STALEPROOF_SCRATCH_URL` unset to use the local one, or point it at another server. The scratch server cannot require a password: docs examples connect to it without one, so a `STALEPROOF_SCRATCH_URL` with credentials is refused (`STALEPROOF_SCRATCH_URL must not carry credentials: ...`). Staleproof also refuses to flush the server named in `FALKORDB_URL`. ## Claude Code (MCP) ```bash claude mcp add staleproof -e FALKORDB_URL=falkor://localhost:6379 -e STALEPROOF_ORG=falkordb -- npx -y staleproof mcp ``` Add `-e GITHUB_TOKEN=...` to raise the GitHub limit for pull requests the graph has not ingested yet. A deployment also serves MCP over Streamable HTTP at `/api/mcp`, for example `claude mcp add --transport http staleproof https:///api/mcp`. [MCP server](mcp.md) covers other clients and the fourteen tools. ## GitHub Action See [examples/staleproof.yml](../examples/staleproof.yml) and [GitHub Action](github-action.md). The action needs no deployment: it starts a FalkorDB in the job. The workflow needs `pull-requests: write` to post its comment; add `ANTHROPIC_API_KEY` as a secret for agent verdicts. - Without a key, the action runs a graph-mode check: impact, clearance, ranking and up to 3 examples before and after the pull request. - To keep decisions, verdicts and runs between jobs, point `falkordb-url` at your deployment's FalkorDB (stored as a secret such as `STALEPROOF_FALKORDB_URL`) and set `org` to its graph. - With `reindex: 'true'` and `falkordb-url`, the action re-indexes the pull request's repository and `also-index` before checking, at the remote default branch head (no ref is passed). Repositories of the graph that are not listed are not re-indexed with them, so cross-repository edges into them are dropped until the organisation is indexed together again. - The action has run end to end on a private test repository with the one-click workflow (no inputs). ## GitHub App A deployment can also run its own copy of the [GitHub App](github-app.md): a check run and a comment on every pull request of the repositories it is installed on, with no workflow file. Register it with `npx tsx scripts/github-app-register.ts` (it writes the credentials to the git-ignored `.env.app`), set the five variables `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_SLUG` and `CRON_SECRET`, and redeploy; `vercel.json` already schedules the App's daily job (`crons`: `GET /api/github/cron`). Without the first three the App's routes answer 404 and nothing else changes. The steps and the commands are in [GitHub App: run your own App](github-app.md#run-your-own-app). ## Production checklist - **A separate scratch database.** Examples run against a FalkorDB that is flushed (`FLUSHALL` and `GRAPH.UDF FLUSH`) before every run. Use a dedicated, unauthenticated server on a private network, or the managed local one (127.0.0.1:6390). Never point it at the org graph's server: Staleproof refuses when the host and port match, but a different address for the same server is not detected. - **Authentication and rate limits.** The API has no authentication, only a limit of 30 POST requests per client per hour, counted in memory per process; `/api/mcp` is not limited. Put an authenticating proxy in front of a public deployment. The limit keys on `X-Real-IP`, else `Fly-Client-IP`, else the last `X-Forwarded-For` entry: have your proxy set `X-Real-IP`, or a client can pick its own key by sending it; without a proxy, all clients share one bucket. - **`GITHUB_TOKEN`.** Without it, ingestion shares GitHub's 60 requests per hour per IP address; through the GitHub API (Vercel) one new pull request costs up to five requests. In hosted mode, use a token limited to public repositories ([why](#a-github-token-for-hosted-mode)). - **Hosted mode's memory.** Workspaces are evicted by the FalkorDB server's total memory, so the curated graph counts toward the 70 MB threshold too. On a larger server, raise the limits through the library (`new Workspaces(store, config, curated, { limits })`). - **Function region next to FalkorDB.** Set `regions` in `vercel.json` to the region nearest your FalkorDB; a check makes dozens of graph round trips. - **Snapshot fallback.** Run `npm run snapshot` against the production graph before you build, so the page a visitor sees when the database is unreachable is recent. It is labelled with the time it was taken. - **Time budget.** On Vercel, keep `STALEPROOF_CHECK_BUDGET_MS` well under the function's 300 s. A function killed mid-check leaves its `Check` node `running`. - **Model settings.** For an Anthropic-compatible provider, set `STALEPROOF_MODEL` to the provider's model; a `claude-*` name is refused and every agent check fails. `GET /api/health` shows the model and provider the server will use. - **Persistent storage.** On a VM, keep `STALEPROOF_HOME` on a volume, or virtualenvs and clones are rebuilt after every restart. Back up the FalkorDB data: verdicts, decisions, runs and checks live only in the graph. - **Re-indexing.** Index all of an organisation's repositories in one `index` command, so cross-repository edges are resolved together. A re-index marks vanished symbols and sections `gone`, and it removes the late links that earlier pull request ingestion created; already-ingested pull requests are not ingested again. - **Isolation for runs.** The sandbox is a guard, not a security boundary: it blocks network and DNS at the Python socket level, the example process inherits no environment, and the install step, which runs the repositories' build code, gets only an allow-list (paths, locale, proxies, certificates, `UV_*` and `PIP_*`), so neither sees `FALKORDB_URL`, model keys or `GITHUB_TOKEN`. Installs do reach the network to fetch dependencies. Run the server in a container when it executes examples from repositories you do not control. ## Related - [Configuration](configuration.md): every variable and its default - [HTTP API](http-api.md): routes, rate limits and snapshot mode - [GitHub Action](github-action.md) - [GitHub App](github-app.md) - [Example runner](example-runner.md): the sandbox and the scratch server - [Troubleshooting and FAQ](troubleshooting.md) --- # Troubleshooting and FAQ This page explains the error messages you can meet when running `staleproof` (each with what it means and how to fix it), the FalkorDB version behaviour the queries are written around, the limits of what the tool does today, and answers to the questions developers ask most. It is for anyone using the CLI, the server, the MCP tools or the library. Messages are quoted as the code writes them; `<...>` marks a value that varies. ## Common errors The CLI prints errors as `staleproof: ` and exits with code 1. The HTTP API returns them as `{"error": ""}`, tools return them as error results, and failed example runs record them as the run's reason. ### Setup and configuration #### FalkorDB is not reachable ```text cannot reach FalkorDB at : Start one locally with `npx staleproof db start` (or scripts/falkordb-local.sh start), or set FALKORDB_URL. ``` Nothing answered at `FALKORDB_URL` (or `--db`) within 5 seconds, or the credentials were refused. The password in `` is shown as `***`. With neither of them set, CLI commands start a local server on `localhost:6379` instead of failing, so from the CLI this error means a URL you configured did not answer, or the local server did not come up. Fix the URL (`falkors://` for TLS endpoints, the password URL-encoded; [URL forms](configuration.md#graph-and-organisation)), or unset it to use the local server. After a connection drops, the client retries four times and then reports `FalkorDB unreachable`. When the automatic start itself fails, the command stops, after the script's own message ([Local FalkorDB does not start](#local-falkordb-does-not-start)), with: ```text could not start a local FalkorDB (see above). Start one yourself, e.g. docker run -d -p 6379:6379 falkordb/falkordb, or set FALKORDB_URL to a FalkorDB you have. ``` #### A query failed `FalkorDB failed on graph "": ` FalkorDB rejected or failed a statement; `` is the name in [Graph model and Cypher](GRAPH-MODEL.md), or `adhoc` for unnamed statements. Check the server version ([version notes](#falkordb-version-notes)) and that the server has the FalkorDB module loaded. #### Invalid organisation name `invalid org name: ""` `--org` or `STALEPROOF_ORG` contains no letter, digit, `_` or `-` (an empty `STALEPROOF_ORG` also gives this). Names are slugged: `Acme Inc` becomes the graph `acme-inc`. #### Invalid effort `STALEPROOF_EFFORT must be one of low, medium, high, xhigh, max` The variable is checked when the configuration loads, so this stops every command except `db`, even with `--effort` on the command line. Fix or unset the variable. `check --effort` itself is not checked locally: an unknown value is sent to the model API as is, and if the API rejects it the check ends with `error`. #### Scratch URL with credentials `STALEPROOF_SCRATCH_URL must not carry credentials: docs examples connect to the scratch server without a password` Docs examples connect to `localhost:6379` without a password, so the scratch server cannot require one. Because the configuration loads in every command, this stops all of them except `db`. Point `STALEPROOF_SCRATCH_URL` at an unauthenticated server on a private network, or unset it to use the managed local server on port 6390. A value that cannot be parsed at all gives `invalid STALEPROOF_SCRATCH_URL: `. #### Broken installation `tree-sitter-python.wasm not found (expected in /grammars/)` (or `tree-sitter-typescript.wasm`, `tree-sitter-tsx.wasm`) A grammar is missing from the package, which happens with a partial install or a copied `dist/` without `grammars/`. Reinstall the package; in a custom image, copy `grammars/` and `python/` next to `dist/`. #### Local FalkorDB does not start ```text falkordb-local: need uv (https://docs.astral.sh/uv/) or Python 3.12+ to install falkordblite; or run FalkorDB in Docker: docker run -d -p 6379:6379 falkordb/falkordb falkordb-local: falkordblite needs Python 3.12+ (python3 is ): install uv (https://docs.astral.sh/uv/), which fetches one, or run FalkorDB in Docker: docker run -d -p 6379:6379 falkordb/falkordb ``` `staleproof db start` (and the managed scratch server, and the local server a command starts when no FalkorDB is configured) installs the `falkordblite` wheel once, with uv or with `python3 -m venv` and pip. The wheel needs Python 3.12 or later: install uv, which fetches a Python 3.12, or run FalkorDB in Docker and leave `FALKORDB_URL` at its default. `falkordb-local: server did not answer PING on port — see /redis.log` The server did not come up within 5 seconds. The log usually says why, for example that the port is already in use. `staleproof db status --port ` reports whether a server is running there. ### Indexing #### Not a repository name `not a GitHub repository: "" (expected owner/name or a github.com URL)` `index` accepts `owner/name`, `https://github.com/owner/name[.git]` and `git@github.com:owner/name[.git]`. #### git failed `git failed in : ` Common causes: - **A `--ref` git cannot check out.** The ref is passed to `git checkout --detach` unchanged. A bare branch name resolves to the clone's local branch, which fetching does not advance; use `origin/`, a tag or a SHA. - **A private or missing repository.** Terminal prompts are disabled, so a private repository fails here unless `GITHUB_TOKEN` (or `GH_TOKEN`) is set to a token that can read it; git then sends it to github.com. Check the name, and the token's access. - **No network.** Clones and fetches need github.com. Without git installed at all, indexing downloads tarballs instead (next section). #### Tarball download failed Without git, or with `STALEPROOF_INGEST=github`, repositories are downloaded as tarballs ([Indexing](indexing.md#without-git-tarballs)): | Message | Meaning | |---|---| | `GitHub: [@] not found (is it public? private repositories need GITHUB_TOKEN)` | GitHub answered 404: a wrong name or ref, or a private repository without a token that can read it | | `GitHub: downloading failed: ` | Any other failed download | | `GitHub: the archive of carries no commit id` | The archive had no pax header with the commit; nothing was indexed | | ` is too large to index from an archive here (; the limit is 150 MB and files). Run Staleproof where git is installed (npx -y staleproof check , which clones instead) or in the GitHub Action.` | The archive is over 150 MB, or it has more than 4000 files the indexer reads (3000 on a hosted server). The CLI writes the same text, so where git is available, install it and index again | #### Repository configuration error `.staleproof.toml: ignore must be a list of glob strings` (or `skip-examples`, or `pyproject.toml: …`) The repository's [configuration](configuration.md#repository-configuration) has a key with the wrong type, for example `ignore = "docs/old"` instead of `ignore = ["docs/old"]`. A file that is not valid TOML stops the index with the TOML parser's message. Fix the file in the repository and index again. ### Pull requests and GitHub #### Not a pull request URL ```text not a GitHub pull request URL: "" (expected https://github.com///pull/) ``` Accepted forms: `https://github.com///pull/`, with an optional trailing `/files`, query or fragment, and `/#`. Issue URLs, commit URLs and URLs of other hosts are refused. The HTTP API answers 400 for this. #### Repository not indexed ```text is not indexed in the "" graph. Run: staleproof index (plus the repositories that document it). ``` The pull request's repository is not in this graph, and the caller does not index on first use: `check --no-index`, `run --pr`, the library, or the check form and `POST /api/check` of `staleproof serve` without `--hosted`. `check`, both MCP servers and hosted servers index the repository themselves. Index it together with the repositories that document it, and check that the graph is the one you indexed into: the CLI and the stdio MCP server default to the graph of the repository's owner, `serve` to `STALEPROOF_ORG`, else `falkordb`. #### GitHub rate limit `GitHub API rate limit reached (60 requests/hour without a token; set GITHUB_TOKEN to raise it). It resets in min.` Unauthenticated GitHub API requests are limited per IP address. Set `GITHUB_TOKEN` (or `GH_TOKEN`). Ingesting a pull request costs one request with git and up to five without it (Vercel, or `STALEPROOF_INGEST=github`); merged pull requests are cached under `STALEPROOF_HOME`. With a token the message is `GitHub API rate limit reached. It resets in min.` The reset time is left out when GitHub does not send it. On a hosted server, whose token you cannot change, a check that hits the limit ends with the same message followed by `This shared server reads pull requests through GitHub's API, which limits it per hour. Until then, run the same check yourself (npx -y staleproof check ) or add the GitHub Action: both use your own GitHub quota.` #### Not found `GitHub: not found (a private repository needs GITHUB_TOKEN with read access)` GitHub answered 404: the pull request number is wrong, the repository is private, or it was renamed. With a token the message is `GitHub: not found`, and a private repository then means the token cannot read it. Other GitHub failures read `GitHub failed: `, and a failed file read reads `GitHub raw @ failed: `. #### Merge commit without a parent `merge commit has no parent` The merge commit GitHub reports for the pull request has no parent to compare against. This is unusual; check the pull request on GitHub. ### Before a pull request | Message | Meaning | |---|---| | `cannot tell which GitHub repository is (origin: ); pass --repo owner/name` | `staleproof check` without a pull request (or `check_changes` with `dir`) could not read a github.com `origin` remote (`origin: none` when there is none). Pass `--repo` (`repo` over MCP) | | ` is not indexed in the "" graph: staleproof index ` | The working tree's repository is not in the graph and indexing on first use is off (`--no-index`) or not available | | `repo (owner/name) is required with files` | `check_changes` with `files` but no `repo` | | `send the changed files: files: [{path, content}] with repo (this server cannot read your disk)` | `check_changes` with `dir` (or with neither) on the hosted server or `staleproof serve`: only the stdio server reads a checkout | | `No live symbol in the "" graph is named .` | `docs-for` (or `docs_for_symbol`) found nothing: check the spelling, the graph (`--repo` or `--org`; a bare name with neither uses `falkordb`), and that the repository is indexed | | `git failed in : …` | The directory is not a git checkout, or `--base` names a ref that does not exist there ([git failed](#git-failed)) | ### Docs audit | Message | Meaning | |---|---| | `not indexed in the "" graph: ` | `audit --no-index`, or `audit_docs`, for repositories the graph does not hold. Drop `--no-index`, or index them | | `unknown --format (text, markdown or json)`, `--run must be a whole number, got `, `--limit must be a whole number, got `, `--fail-under must be a score from 0 to 100, got ` | A bad option value; nothing ran | | `health score is below --fail-under `, ` example(s) no longer fit the code (--fail-on-mismatch)` | Not errors: the reason `audit` exits 1 after printing the report | | `… Indexed by an older Staleproof: examples are not checked against the code until the repositories are indexed again (staleproof index).` | The graph was written before Staleproof 0.3, so it has no `MISMATCH` edges or export flags and the audit cannot judge the examples. Index the repositories again | | A grey `docs health: not audited` badge | The server behind the badge URL has not indexed the repository. Audit it there once ([the page](https://staleproof.dev/#/audit)); the badge is cached for 5 minutes | | `Could not open the audit issue (the job needs issues: write). The report is in the job summary.` | The GitHub Action's workflow lacks `permissions: issues: write` | ### Agent and models #### Agent mode without credentials ```text the agent needs model API credentials: set ANTHROPIC_API_KEY (or run `ant auth login`) for the Claude API, or ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY and STALEPROOF_MODEL for an Anthropic-compatible provider such as DeepSeek. Use `check --no-agent` to run the graph tools without a model. ``` `check` runs agent mode by default and found no credentials ([where it looks](configuration.md#model-agent-mode)). Set a key, or run `check --no-agent`. The same message with the API's own error appended in parentheses means the key was found but rejected; the check then ends with status `error`. On the server, the equivalent is `409 {"error": "agent mode needs model API credentials on the server (ANTHROPIC_API_KEY); graph mode works without them", "agentUnavailable": true}` for `mode: "agent"`. Graph mode needs no credentials. A hosted server also limits agent checks per day: `429 {"error": "today's hosted agent checks are used up; graph mode still works, and the agent runs anywhere with your own key (npx -y staleproof check )", "agentUnavailable": true}`. The count resets at midnight UTC. #### A Claude model name on a compatible provider ```text "claude-sonnet-5-5" is a Claude model name, but the model endpoint is an Anthropic-compatible provider, which would answer with its own model under that name; set STALEPROOF_MODEL to the provider's model (e.g. deepseek-v4-pro) ``` `ANTHROPIC_BASE_URL` points at a host other than `api.anthropic.com`, and the model name starts with `claude-` (here the default, because `STALEPROOF_MODEL` is unset). Set `STALEPROOF_MODEL` (or `--model`) to the provider's model, for example `deepseek-v4-pro`. The server still reports `agent.available: true` in this state, and every agent check fails with this message as an `error` event. If `ANTHROPIC_BASE_URL` is not a valid URL at all, the server treats it as a compatible provider and `GET /api/health` answers 500 (`Invalid URL`). #### The check stopped early These are summaries of checks that ended `incomplete`, not errors; the sections still open are listed in `missing`: - `Stopped after 40 model turns with impacted section(s) still open.` Raise the limit with `check --max-steps ` (the server always uses 40). - `Stopped at this server's 240 s time limit after model turns with impacted section(s) still open; the CLI has no time limit.` The server's `STALEPROOF_CHECK_BUDGET_MS` (240 s by default on Vercel) ran out. Run large checks from the CLI. - A check where the model finished with sections still open, after two reminders. Each reminder appears as a `nudge` event. `The model declined to continue ().` is the summary of a `refused` check: the model stopped with a refusal. #### A tool call was cut off `The check failed: a tool call was cut off at max_tokens; raise max_tokens and re-run the check` A model turn hit its output limit (32,000 tokens) in the middle of a tool call. The CLI has no option for the limit; the library's `AnthropicModel` takes `maxTokens`. Re-running often succeeds, because the model chooses differently. #### Errors the model sees and recovers from These come back to the model as error results, so a check continues; they also appear in the trace and, over MCP, to your client: | Message | Meaning | |---|---| | `unknown tool ` | The tool does not exist | | ` has no parameter ; its parameters are ` | An argument the tool does not declare. The agent loop and the MCP server both refuse it instead of dropping it | | `invalid input for : : ` (MCP: `invalid input: ...`) | The input failed the tool's schema; `give section_id or section_ids` is the `record_verdict` rule | | ` has not been checked yet; call impact first` | `run_example` with a `pr_url` the graph has not ingested. Call `impact` first; `staleproof run --pr` does it for you | | `no code example ` | No example with that id (`owner/repo:path:index`) | | `refused: no path in the graph from 's changes to
. Staleproof never edits a paragraph without a path; record_verdict instead if it needs no change.` | `propose_edit` on a section the pull request does not reach within 3 call hops | | `new_text is identical to the current section; use record_verdict with ok` | The proposed edit changes nothing | | `a governed verdict must cite the decision id(s)` | `record_verdict` with `governed` needs `cites` | | `pr_url is required` | `propose_edit` or `record_verdict` without a pull request. In agent mode it defaults to the pull request under check; over MCP and in the library pass it | | `no doc section or pull request in the graph` | The section id or the pull request is unknown | | `none of is a DocSection or Symbol id; decision not recorded` | `write_decision` (or `decide`, or `POST /api/decisions`) with no valid id in `governs` | ### Example runs A run that cannot be prepared is recorded with status `error`, and `run_example` reports `The sandbox could not be prepared: `. Error runs are never answered from the cache, so the next request tries again. #### Scratch server is the org server ```text refusing to flush :: it is the server holding the org graph (FALKORDB_URL). Point STALEPROOF_SCRATCH_URL / STALEPROOF_SCRATCH_PORT at a separate server. ``` Every run starts by flushing the scratch database, and its host and port equal those of `FALKORDB_URL`. Staleproof refuses rather than delete your graph. Unset `STALEPROOF_SCRATCH_URL` to use the managed scratch server on port 6390, or point it at another server. #### Install dependency conflict ```text dependency conflict: requires , which the other checkouts don't satisfy (installing , ) ``` The repositories an example needs cannot be installed together at those commits. On FalkorDB's repositories, `graphrag-sdk==1.4.0` at its indexed commit requires `falkordb[hiredis]>=1.8.0,<2`, which the falkordb-py checkout under test does not satisfy, so three examples of falkordb-py#276 could not be run ([FACTS](FACTS.md#example-runs-sandbox)). The pull request is not to blame: it is version skew between the checkouts. There is nothing to fix in Staleproof; the agent records such sections as `skipped`. Other install failures read `installing failed:` followed by the end of the installer's output, and a failed virtualenv reads `uv venv failed: ...` or `python3 -m venv failed: ...`. #### Network refused in the sandbox `Staleproof sandbox: outbound network is disabled (tried :); only FalkorDB on localhost:6379 is available` `Staleproof sandbox: DNS is disabled ()` The example tried to reach a server other than FalkorDB on `localhost:6379` (which the sandbox redirects to the scratch server). The run is recorded as `fail` with this error. Examples that call external services are not runnable here; when the code mentions `requests.get(`, `httpx.post(`, `boto3`, `neo4j` and similar, it is skipped before it runs with `calls an external service`. #### Timeouts A run that does not exit within `STALEPROOF_RUN_TIMEOUT_MS` (30 s by default) is killed and recorded as `timeout` with the reason `no exit after 30s`. Raise the variable for slow examples. Creating a virtualenv and installing into it have fixed limits of 300 s and 600 s. #### Not executed on this host `this deployment cannot execute examples (no git/uv sandbox runtime); runs recorded by other Staleproof hosts are answered from the graph` The host lacks `git`, or both `uv` and `python3` (Vercel, for example). Recorded runs for the same program and commits are still answered. Run examples from a host that has them (`scripts/seed.sh`, `staleproof run`) to record them on the graph. #### An async example fails with a SyntaxError `SyntaxError: 'await' outside function`, or `'async with' outside async function` Since 0.2.0, examples with a top-level `await`, `async with` or `async for` run on an event loop ([Async examples](example-runner.md#async-examples)). A run recorded before that has the same program and commits, so it is answered from the graph with its old error. Re-run it with `staleproof run --pr --fresh`. #### Skipped runs A skipped run is not an error: it says why the example cannot prove anything. The reasons are `not Python ()`, `empty example`, `needs an LLM provider (); Staleproof does not hold provider keys`, `reads a local file the docs do not provide ()`, `contains placeholders to fill in (e.g. "")`, `waits for user input`, `calls an external service`, `uses no symbol from an indexed repository`, and `fragment: (the page never defines it)` for a fragment that uses a name nothing on its page defines. [Example runner](example-runner.md) has the exact rules. ### Docs patch #### Not checked ` has not been checked` `patch` (or `GET /api/patch`, or `docs_patch`) for a pull request the graph has never ingested. Run a check first. #### No proposed edits `patch` prints `(no proposed edits)` when no section has a `stale` verdict with new text that a maintainer has not rejected. Graph mode records no verdicts, so the patch of a graph-mode-only pull request is always empty. On falkordb-py #262, #267 and #276 the patch is empty too: every verdict the agent recorded on them is `ok`. #### Edits not applied The patch description lists them under "Not applied (file changed since indexing)", and the API under `conflicts`: - ` changed since it was indexed at `: the section's text at the indexed commit differs from the text the verdict was written against, usually because the docs repository was re-indexed after the check. Check the pull request again so the edit is proposed against the current text. - ` not found in @`: the file is missing at the indexed commit. - A section longer than 8,000 characters always conflicts: its stored text is cut at 8,000 characters, so it never matches the file. #### Opening the pull request - `opening a pull request needs GITHUB_TOKEN`: set `GITHUB_TOKEN` or `GH_TOKEN`. - `the patch is empty: no proposed edits to open a pull request for`: there is nothing to open. - `fork did not become available`: GitHub did not finish creating the fork within 30 polls 2 seconds apart. Run the command again; the existing fork is reused. - `--upstream and --in-repo are different targets: pick one`: pass one of them. - `the docs fix was not opened: the target branch has other text where each edit applies (...)`, or `left out of the pull request in : : has other text where the edit applies (the edit was written against )`: with `--in-repo`, the edits were written against the indexed commit and `--base` (the checked pull request's branch) changed those lines since. Nothing of the branch is overwritten. Check the pull request again (re-index first if the default branch moved) so the edits are written against what it changed, then open the pull request again. - `no pull request opened: no proposed edit is in a repository this token may write to`: with `--in-repo --only `, every proposed edit is in another repository. Open those with `patch --open-pr` from an account that can write to them. - `Could not open the docs fix pull request (the job needs contents: write and pull-requests: write, and the repository must let GitHub Actions create pull requests; see the log above).`: the GitHub Action's `open-docs-pr` step, without those permissions, on a branch the token cannot push to, or in a repository that does not let GitHub Actions open pull requests (the next entry). - `GitHub Actions may not open pull requests in , so the fixes wait on the branch staleproof/fix-docs: ...` (`mode: fix`, also `staleproof fix-pr` with a workflow's token), or `... so the docs fix waits on the branch staleproof/docs-...` (`open-docs-pr`): GitHub refused the pull request with `403 GitHub Actions is not permitted to create or approve pull requests`. New repositories and organizations have this off. The branch is pushed; only the pull request is missing. Turn on **Allow GitHub Actions to create and approve pull requests** (Settings → Actions → General → Workflow permissions; when it is greyed out, the organization's settings turn it off), or pass a GitHub App or personal access token as `github-token`. The message ends with the link that opens this one by hand; the next run after the change opens it on its own. - `staleproof fix-pr failed: . The job needs contents: write and pull-requests: write (see examples/staleproof-fix.yml).`: the Action's `mode: fix` step; `` is the CLI's message (the entries above, or a GitHub error). ### Hosted service These come from a server in hosted mode (staleproof.dev, `serve --hosted`): as an `error` event in the stream of `POST /api/check` or `POST /api/index`, as an MCP error result, or as `400 {"error": ...}` elsewhere. The limits are in [Deploy](DEPLOY.md#limits). | Message | Meaning | |---|---| | `the hosted workspace gh- would hold repositories (limit 4: ). Run Staleproof yourself for bigger organisations: npx -y staleproof index ` | A workspace holds at most 4 repositories, counting the ones already in it | | ` came to MB of graph, over the hosted limit of 20 MB. Run it on your own FalkorDB: npx -y staleproof check , or the GitHub Action.` | The indexed workspace was too big; it was dropped | | `the hosted service is full right now. Run Staleproof on your own machine or in CI instead: npx -y staleproof check , or the GitHub Action.` | Evicting the least recently used workspaces did not bring the server under 70 MB | | `the hosted service has indexed 500 workspaces today, its daily limit. Run it yourself: npx -y staleproof check ` | The daily indexing budget is spent; it resets at midnight UTC | | `gh- is being indexed by another request; try again in a minute` | Another request held the workspace's lock for more than 150 seconds | | ` is in the curated "" graph, which the hosted service does not re-index; index other repositories on their own.` | `with` named repositories for a repository of the curated graph | | ` is too large to index from an archive here (...)` | The archive is over 150 MB or has more than 3000 files the indexer reads ([above](#tarball-download-failed)) | | `name a repository: pass a pull request URL (pr_url), repo or repos ("owner/name"), or ids that start with owner/repo:` | MCP: the call names no repository, so the hosted server cannot pick a graph | | ` is not indexed yet: call check_pull_request with one of its pull requests first` | MCP: a tool that does not index on first use (for example `read_section` or `docs_patch`; the indexing ones are `check_pull_request`, `impact`, `clearance`, `check_changes`, `docs_for_symbol` and `audit_docs`) for a repository no workspace holds | | `404 this server indexes from the CLI only: staleproof index ...` | `POST /api/index` on a server that is not in hosted mode | ### Server and web app | Status and message | Meaning | |---|---| | `429 rate limited: try again later` | Over 30 POST requests from one client in the last hour ([rate limits](http-api.md#rate-limits)) | | `404 no such check`, `no such section`, `no such verdict` | The id is unknown in this graph (in hosted mode, in every graph); check `--org` | | `404 is not indexed` | Hosted mode: `GET /api/patch` or `POST /api/decisions` for a repository no graph holds | | `404 is not indexed yet: POST /api/audit {"repo": "owner/name"} indexes and audits it` | `GET /api/audit` for a repository no graph holds (without hosted mode: `…: staleproof index `) | | `400 symbol required, e.g. ?symbol=Graph.query&repo=owner/name`, `400 repo required (owner/name), or a symbol id that starts with owner/repo:` | `GET /api/docs-for` without `symbol`, or in hosted mode without a repository | | `400 id required` | `GET /api/sections` without `id`. `GET /api/patch` without `pr` answers `400 not a GitHub pull request URL: ""…` | | `400 status must be accepted or rejected` | The review body needs `status` | | `400 decision text is limited to 500 characters` | The HTTP API caps decision text; the CLI `decide` does not | | `503 the Staleproof API is not connected to FalkorDB (); set FALKORDB_URL` | Vercel only: the function cannot reach FalkorDB | The CLI's `verdict` command reports `action must be accept or reject` and `no verdict `. The web app shows `the Staleproof API is unreachable and no snapshot is bundled` when the API fails and there is no snapshot for that page, and `Live checks need the Staleproof API connected to FalkorDB; this deployment is showing a snapshot. Run it locally: npx staleproof serve` when you try to run a check or record a decision in [snapshot mode](http-api.md#snapshot-mode). ## FalkorDB version notes Staleproof has run on two FalkorDB versions: 4.18.3 (the falkordblite build that `staleproof db start` and the test suite use) and 4.22.0 (FalkorDB Cloud, where [FACTS.md](FACTS.md) was generated). Each has a traversal behaviour the queries are written around. - **4.18.3: an unbound variable-length pattern can drop nodes.** When a node is reachable along two routes of different length, a pattern such as `(err)<-[:CALLS*0..3]-(t)` that binds no path variable can miss some of the callers. In GraphRAG-SDK, `RetrievalStrategy.search` calls `RetrieverError` directly and through `_validate`, and the pattern found 4 of the 6 callers. Clearance then used `WHERE NOT (pattern)` and "proved" a symbol undocumented while the docs' quickstart was three calls away. Binding the path (`MATCH p = (s)<-[:CALLS*0..3]-(t)`) gives the right answer on both versions, so every variable-length pattern in Staleproof binds one, and clearance counts bound paths (`OPTIONAL MATCH p = ... WITH count(p) AS reach WHERE reach = 0`) instead of using a pattern predicate. - **4.22.0: an exact length of 2 or more toward a labelled node returns nothing.** For example `MATCH (:N {name: 'd'})<-[:R*2]-(t:N)` returns no rows on the chain `a->b->c->d`. No Staleproof query uses an exact length: every range starts at 0 or 1. [test/traversal.test.ts](../test/traversal.test.ts) reproduces the GraphRAG-SDK shape and fails if any query reintroduces an unbound variable-length pattern or a range that does not start at 0 or 1. Follow the same two rules in your own Cypher. Other version-dependent behaviour: - `rank_sections` calls `algo.pageRank`; on any error it falls back to counting inbound links per page and reports `method: "in-degree"`. - Between example runs the scratch server is cleared with `FLUSHALL` and `GRAPH.UDF FLUSH`; servers without UDF support reject the second command, and that error is ignored. - `GET /api/health` reads the version with `MODULE LIST` and shows `unknown` when the server does not allow it. - [docker-compose.yml](../docker-compose.yml) uses `falkordb/falkordb:latest` unless `FALKORDB_IMAGE` is set, so the version you get there has not been tested; pin `FALKORDB_IMAGE` for reproducible runs. ## Limits Stated plainly; most are "not yet" rather than "never". **What is analysed** - **Python, TypeScript, JavaScript and Go code.** Python is read anywhere in a repository; TypeScript and JavaScript only inside a published npm package (a `package.json` with a `name`, not `private`); Go in every package under a `go.mod`. Other languages are not parsed, and notebooks are not read. - **Argument checks follow each language.** TypeScript's optional, default and rest parameters are respected, and in JavaScript files every parameter is optional, so a JavaScript example is flagged for too many arguments only past a rest-free signature; overloads, decorators and functions that read `arguments` are not judged. Go examples are checked (variadic aware) but not run; JavaScript and TypeScript examples run in Node ([JavaScript and TypeScript](example-runner.md#javascript-and-typescript)). - **reStructuredText is scanned, not built.** Titles, code directives (`code-block`, `testcode`, `doctest`, …), `::` literal blocks, `>>>` doctests, API roles and `:doc:`/`:ref:` links are read. `.. include::`, autodoc output (`.. autofunction::` and the like), substitutions and Sphinx extensions are not expanded, and `:ref:` resolves only to labels on the same page. - **Static checks follow exact resolutions only.** An example call is judged only when it resolved through imports and types, to a function whose signature no unknown decorator wraps, on a class or module whose members are all visible, without `*args`/`**kwargs` splats. The rest is silent, so no mismatch and no predicted break is not proof that an example works; runs are. - **Static call resolution.** Calls are resolved through imports, re-exports, attribute types, return annotations and base classes. Where the types do not say, a method call falls back to a by-name edge when 1 to 4 functions of that name exist in repositories the module imports; such edges are labelled `by-name` everywhere. Generic names (`get`, `run`, `close`, ...) are never matched by name. - **Cross-repository resolution needs the repositories indexed together.** Edges between repositories are resolved only within one `index` command. Indexing a repository alone (including indexing on first use without `--with`, and the Action's `reindex` without `also-index`) drops its cross-repository edges until the organisation is indexed together again. - **Re-index semantics.** A re-index re-resolves calls and doc links and marks vanished symbols and sections `gone`. It also removes the late links that pull request ingestion created for removed symbols, and already-ingested pull requests are not ingested again (unless an open one moves). The Action's `reindex` uses the remote default branch head, not the base branch. - **Change detection.** `export-removed` is reported for any module that stops importing a name from its own package, not only `__init__.py`, so it can flag internal refactors. A moved or renamed file is reported as every definition removed and added, because renames are paired only within the same parent. - **Private repositories need a token.** The CLI, a server you run and the GitHub Action read private GitHub repositories with `GITHUB_TOKEN` (the Action's default token reads its own repository). The hosted site, staleproof.dev, and its MCP endpoint index public repositories only; the [GitHub App](github-app.md) reads the private repositories it is installed on, into a graph per installation that no public endpoint can read. Nothing outside GitHub is read. **Runs** - **The sandbox is a guard, not a security boundary.** The example refuses network connections and DNS at the Python socket level, runs with no inherited environment and a temporary HOME, and its process group is killed on timeout. The install step before it runs the repositories' build code with network access and an allow-listed environment (paths, locale, proxies, certificates, `UV_*`, `PIP_*`; [details](configuration.md#example-runner)), without `FALKORDB_URL`, model keys or `GITHUB_TOKEN`. Run it inside a container for untrusted code. - **Runs use today's third-party dependencies.** The repositories are installed from source at the exact commits; their dependencies resolve at install time, so an old commit can fail for reasons unrelated to the pull request. A recorded run is reused for the same program and commits even if the dependencies have moved since; `staleproof run --fresh` re-runs it. - **Without uv, Python is not pinned.** The fallback uses whatever `python3` is installed. - **Graph mode samples.** It ranks the first impact page (up to 40 sections) and runs at most 3 exactly-linked examples on it. It writes no verdicts. **Checks and patches** - **Proposed edits are tested on fixtures, not shown on the live graph.** The agent proposing a docs fix with its evidence path is tested on the project's fixture organisation. On FalkorDB's own repositories, every verdict on falkordb-py #262, #267 and #276 is `ok`: those pull requests fixed code, and no edit was proposed. - **Model paths.** The agent was written for Claude; the Claude API path is tested against a local fake API only. It has run on `deepseek-v4-pro` through DeepSeek's Anthropic-compatible API. The GitHub Action's pull request check has run end to end on a private test repository; the Docker image is written but has not been built. - **An agent may edit an already-broken example only when it proved it.** Besides what a pull request falsified, the agent may fix an example that it ran and that fails both before and after the pull request, saying so in the reason and citing both runs. The path guard still applies: no edit without a path from the pull request's changes to the section. - **Sections over 8,000 characters always conflict in the patch**, because their stored text is truncated. - **The hosted service runs no examples.** Checks and audits on staleproof.dev and its MCP endpoint report impact, predicted breaks, clearance, ranking and the static checks; runs come from the GitHub Action, the CLI or a local MCP server. - **Checks before a pull request are static.** `check` without a URL, `check_changes` and `docs-for` predict and trace; they run no example and record nothing. - **Pull requests are re-read only when they move.** A later check reuses the stored diff unless the pull request is open and its head changed (or it was merged since); `impact` and the other tools alone never re-read it. The library's `ingestPullRequest` always re-ingests. The GitHub Action's default FalkorDB starts empty on every run, so there each run diffs the pull request afresh. - **Verdicts written over MCP** have no check id, so no check counts them. - **Verdict ids are not printed** in the CLI's text output or in the pull request comment; they are in the trace (`check --json`, `GET /api/checks/`). - **The HTTP API has no authentication**, and its rate limit is in memory, per process. ## FAQ ### Which languages does it support? Code in Python, Go, and TypeScript and JavaScript in published npm packages; docs in Markdown, MDX and reStructuredText (Sphinx) from any repository. Python, JavaScript and TypeScript examples are linked to code, checked against its signatures and run; Go examples are linked and checked but not run. Other languages are not parsed. ### Do I need an LLM API key? No. Indexing, graph-mode checks (`check --no-agent`), example runs, the patch, the MCP server and the HTTP API's graph mode need no key. Over MCP, your own client's model (Claude Code, for example) drives the tools. Only agent mode, where Staleproof's loop calls a model, needs credentials. ### Which models can run the agent? The agent was written for Claude: the default model is `claude-sonnet-5-5`, with adaptive thinking and the server-side refusal fallback. It runs on any Anthropic-compatible API through `ANTHROPIC_BASE_URL`, sending only the portable request fields; the live demo runs `deepseek-v4-pro` through DeepSeek's Anthropic-compatible endpoint. The Claude path is tested against a local fake API only. Any model works through the library by implementing `ModelClient` ([library](library.md#agent-and-checks)). ### What does a check cost? There is no published figure. Every agent check records the tokens it used: the CLI prints the total at the end, the `check_finished` event has `usage` (input, output, cache reads), and the `Check` node stores `input_tokens` and `output_tokens`. Multiply by your provider's prices. Spending is bounded by the turn limit (40 by default), the output limit per turn (32,000 tokens) and the size of each tool result handed back (24,000 characters). Graph mode uses no model and costs nothing beyond your FalkorDB and GitHub usage. ### What is sent to the model provider? In agent mode: the system prompt, the tool definitions, the pull request URL, a one-sentence summary of what changed in the graph since the previous check (pull request URLs, counts, and the text of new decisions), and every tool result the model asks for. Tool results contain the text of doc sections and their code examples, symbol names and signatures, evidence paths, example run results (status, the last line of the error, the end of the traceback and of the output), and decisions with their text, author and source. All of it comes from the public repositories you indexed, plus what maintainers wrote into decisions. Your `GITHUB_TOKEN`, model key and other environment variables are never part of a tool result; example processes inherit no environment, and the install step that builds the repositories gets only an allow-list without those secrets. Graph mode sends nothing to a model. ### Does it open pull requests on its own? No. A check writes verdicts and proposed edits to the graph only. `staleproof patch` prints the diff and the description; `patch --open-pr` opens the pull request, and only when you pass that flag: in your fork by default, against the original repository with `--upstream`, or as a branch of the docs repository itself with `--in-repo`. The GitHub Action posts or updates one comment, keeps one audit issue in audit mode, and opens a docs fix pull request into the checked pull request's branch only with `open-docs-pr: 'true'`. ### How long does a check take? The impact query itself takes about 1 to 2 ms server-side on FalkorDB 4.22 ([FACTS](FACTS.md#impact-query-timing-server-side-falkordb)). The rest depends on the model and on example runs. A 43-section agent check takes about 6 minutes; the server on Vercel stops agent checks at 240 s, and the CLI has no time limit. Each example run is limited to 30 s by default, and the first run against a new set of commits also builds a virtualenv. ### Can I use private repositories? Yes, with the CLI, a server you run and the GitHub Action. Set `GITHUB_TOKEN` (or `GH_TOKEN`) to a token that can read them: it is sent to the GitHub API, to `raw.githubusercontent.com` and, through git's environment configuration, to git clones of github.com ([how](configuration.md#github)). In the Action the workflow's default token already reads its own repository. The hosted site, staleproof.dev, indexes public repositories only. ### Do I need to set up FalkorDB? No. The CLI starts a local FalkorDB (falkordblite) when no `FALKORDB_URL` is set and nothing answers on `localhost:6379`; the GitHub Action starts one inside the job; the website and the hosted MCP server bring their own. Set up your own only to keep one graph for a team, as in [Deploy](DEPLOY.md). ### Can one server hold several organisations? Yes. Each organisation is its own graph on the same FalkorDB server, selected with `--org` or `STALEPROOF_ORG` (the CLI defaults to the repository's owner); a second graph built from encode/httpx sits next to `falkordb` on the same server. Graphs never share edges, verdicts or decisions. One `staleproof mcp` process, or `staleproof serve` without `--hosted`, serves one organisation; run one per organisation. `serve --hosted` answers for every owner from one process, indexing each into its own workspace graph on demand. ### How do I measure precision? Review verdicts: `staleproof verdict accept ` or `verdict reject ` (with `--by `), or `POST /api/verdicts//review`. Each review sets the verdict's `status`, `reviewed_by` and `reviewed_at`, and a rejected edit is left out of the patch. Verdict ids are in the check's trace: `check --json` or `GET /api/checks/`, in the `tool_result` of `propose_edit` (`data.id`) and `record_verdict` (`data.ids`). Count reviews by verdict with Cypher: ```cypher MATCH (v:Verdict) WHERE v.status IN ['accepted', 'rejected'] RETURN v.verdict AS verdict, v.status AS status, count(v) AS n ORDER BY verdict, status ``` The [library](library.md#run-your-own-cypher) shows how to run it from Node.js. ### Why FalkorDB? Because the question is a graph traversal. Impact is a variable-length path from a changed symbol through callers in any repository to a doc section; clearance is the same pattern counted to zero, a proof that no doc is reachable; decisions govern symbols transitively through `CALLS*0..2` and resolve through `SUPERSEDES` chains; ranking runs PageRank over the docs' links. FalkorDB runs the impact query in about 1 to 2 ms server-side, and its multiple graphs per server give one graph per organisation. Every statement is in [Graph model and Cypher](GRAPH-MODEL.md). ### Does a re-check notice new commits on the pull request? Yes, since 0.3.0. Before a check (`staleproof check`, `POST /api/check`, MCP `check_pull_request`) of an open pull request, Staleproof compares its head with the one it ingested; when it moved, the old diff is dropped and the current one is read, and the CLI logs ` has new commits since it was last checked: reading its current diff`. `impact` and the other tools on their own reuse the stored diff, so start with a check. A merged pull request is never re-read. Example runs are still answered from recorded runs of the same program and commits; `staleproof run --fresh` re-runs one. ### Can I check changes before opening a pull request? Yes, statically. `staleproof check` with no URL checks the working tree of the checkout you are in, and `--fail-on-predicted` makes it a pre-push hook; `staleproof docs-for ` lists the docs that depend on a function before you change it; over MCP, `check_changes` takes edited files and `docs_for_symbol` a symbol. They predict which examples break; running examples before and after needs the pull request ([CLI](cli.md#check-without-a-pull-request)). ### Why is an example I know is wrong not flagged? The static checks report only what the source settles: a call reached by name rather than through imports and types, a function behind an unknown decorator, a class with an external base or a `__getattr__`, or a call with `*args`/`**kwargs` is left alone, and JavaScript calls get no argument checks ([the rules](indexing.md#static-checks-of-examples)). Run the example (`staleproof run `, or `audit --run `) to prove it, and check that it is not marked `skip` or matched by `skip-examples`. ## Related - [Configuration](configuration.md): every variable and its default - [Deploy](DEPLOY.md): hosts and the production checklist - [Example runner](example-runner.md): the sandbox, skip rules and run outcomes - [The agent](agent.md): the loop, its stop condition and model providers - [HTTP API](http-api.md)