Sync Docs — correct documentation against code reality
sync-docs is a correct op-cell: restore the invariant that public docs, examples, versions, and changelog entries describe the current code. It starts from a diff, maps the changed code to coupled Markdown, applies only the safe subset, and flags every semantic drift item with evidence.
Safe means text substitution with a mechanically known target: version-number bump and CHANGELOG ## [Unreleased] entry. Everything else is flag-only until a human or implementer verifies the intended prose change.
When to Apply / NOT
Apply when a branch changes public APIs, imports, CLI flags, config names, package versions, exported symbols, examples, or user-visible behavior and docs may lag.
NOT apply for generated documentation refreshes, mass prose rewrites, API design review, release-note authorship from scratch, or docs whose source of truth is intentionally external. For those, report drift signals but do not edit.
Inputs
- Mode:
report (default) or apply. apply still edits only safe-fix issues.
- Scope:
recent: changed files from the current branch or last few commits.
before-pr: branch diff against the PR base; use before publishing.
all: scan all tracked code and docs; slower, useful after migrations.
- Base: explicit base ref is preferred. If absent, resolve the default branch, then fall back to
HEAD~5 for recent.
Workflow
Pick scope and base. Refuse ambiguous review scope when the user expects PR readiness. Use these commands as the runnable recipe:
# before-pr: whole branch against the remote default branch
remote_head=$(git symbolic-ref --short refs/remotes/origin/HEAD || printf origin/main)
base=${remote_head#origin/}
git diff --name-status "origin/$base"...HEAD
# recent: if the default-branch diff fails, run the fallback explicitly
git diff --name-status HEAD~5..HEAD
# all: tracked docs and code inventory, not a behavioral diff
git ls-files
In ODIN tool mode, use bash only for the git commands; use find, search, read, lsp, ast_grep, and edit for everything else.
Compute changed code. Keep only source/config/package files that can change docs. Exclude pure docs, vendored/generated paths, lockfiles unless version docs mention package manager output, and deleted files that were never public.
Output shape:
{"status":"M|A|D|R","oldPath":"src/old.ts","path":"src/new.ts","basename":"client","modulePath":"src/new","kind":"source|manifest|config|cli"}
Extract coupling terms. For each changed code file, derive:
filename stem: client, auth-server.
full path and path without extension: src/auth/client.ts, src/auth/client.
import strings from the diff: from "pkg/auth", require("pkg/auth"), dynamic imports.
exported/public symbols from codegraph_search / codegraph_explore when indexed.
fallback symbols from language syntax:
ast-grep --pattern 'export function $NAME($$$)' --lang ts <file>
ast-grep --pattern 'export class $NAME { $$$ }' --lang ts <file>
ast-grep --pattern 'pub fn $NAME($$$)' --lang rust <file>
git grep -nE '^(export |pub |def |class |func )' -- <file>
Discover related docs. Search only live doc surfaces first: README.md, CHANGELOG.md, docs/**/*.md, *.md at repo root. For each coupling term, search docs and record doc, line, term, referenceType (filename, full-path, import, symbol, url-path, version). Default ignore list lives in references/doc-issues.md.
Runnable fallback:
git grep -n -- '*.md' README.md CHANGELOG.md docs/ -- '<term>'
git grep -nE 'from ["'"''][^"'"'']+["'"'']|require\(["'"''][^"'"'']+["'"'']\)' -- '*.md'
ODIN tool equivalent: search each escaped term across README.md, CHANGELOG.md, docs/**/*.md, then read the surrounding lines.
Classify issues. Use the taxonomy in references/doc-issues.md. Preserve certainty:
- HIGH: manifest version mismatch with exact current version; deleted/renamed public export still documented with symbol proof; changed import path in fenced example where the old path no longer resolves.
- MEDIUM: stale code example that mentions a changed file/symbol but needs semantic review; undocumented public export after entry-point/internal filtering; docs describing codegraph-reported dead code.
- LOW: doc-drift from zero code-coupling or weak filename-only coupling; broad stale prose suspicion.
Apply safe fixes only. In apply mode:
- Version bump: replace stale semver strings in docs with the manifest version when the line clearly labels a version (
version, package badge, install snippet with @x.y.z). Avoid broad numeric replacement.
- CHANGELOG
## [Unreleased] entry: insert a minimal bullet under the existing section, or create the section at the top if absent. Use commit messages or changed-file summaries; do not invent product claims.
Do not auto-edit removed exports, import paths, examples, undocumented exports, dead-code docs, or doc-drift prose. Those require intent.
Flag the rest. Emit a compact report sorted by severity, then file path:
HIGH docs/api.md:42 removed-export `createClient` no current public symbol; changed in src/client.ts
MEDIUM README.md:88 stale-code-example imports old path `pkg/client`; verify replacement `pkg/auth/client`
LOW docs/legacy.md:? doc-drift zero code-coupling; no live filename/import/symbol references
Return fix ledger. For every edit, record file, line, type, before, after, and evidence source. For every flag-only item, record reasonFlagOnly.
Native Detection Recipes
Code graph first
When the repo is indexed, use the codegraph MCP for symbol truth:
codegraph_search: locate changed public symbols by name.
codegraph_callers / codegraph_callees: determine whether docs describe dead wrappers or removed API surfaces.
codegraph_impact: judge blast radius before calling an export undocumented.
codegraph_files: confirm entry points and module layout.
Fallback when not indexed:
git grep -nE 'export (function|class|const|let|var)|export \{|module\.exports|pub (fn|struct|enum|trait)|^def |^class ' -- ':!node_modules' ':!generated'
ast-grep --pattern 'import $X from $Y' --lang ts src
ast-grep --pattern 'from $M import $$$N' --lang python .
Manifest versions
Read manifests directly and compare against doc mentions:
- JavaScript/TypeScript:
package.json version.
- Rust:
Cargo.toml package.version.
- Python:
pyproject.toml project.version, fallback setup.cfg / setup.py only as MEDIUM.
- Go: module version is usually tag-derived; do not auto-fix unless a manifest line gives an exact version.
Search docs for labeled semver:
git grep -nE '(version|v|@)[[:space:]:="'"'']*[0-9]+\.[0-9]+\.[0-9]+' -- '*.md'
CHANGELOG evidence
Use commits only as evidence, not as prose authority:
git --no-pager log --oneline --no-merges "origin/$base"..HEAD
git diff --name-only "origin/$base"...HEAD
If CHANGELOG.md has no ## [Unreleased], create one immediately below the title. If it exists, insert under an appropriate subsection only when the subsection already exists; otherwise add a plain bullet under ## [Unreleased].
Anti-patterns
- Regex-only confidence on public API removal: require codegraph, LSP, ast-grep, or exact diff evidence before HIGH.
- Mass replacing version-looking numbers: examples, ports, years, protocol versions, and dates are not package versions.
- Fixing examples by guess: changed import path is usually flag-only unless the diff explicitly contains a one-to-one rename.
- Treating CHANGELOG as drift: append-only history has intentionally low code coupling.
- Reporting generated/versioned docs as stale: they are snapshots unless explicitly in scope.
- Inventing release notes: changelog bullets must cite commits or changed files.
Validation Gates
| Gate |
Pass Criteria |
Blocking |
| Scope resolved |
Base ref, scope, changed files, and ignored paths are listed |
Stop with exact missing base or empty diff |
| Evidence attached |
Every issue has file:line when a line exists, plus the changed code source |
Do not report line-specific claims without a read/search hit |
| Safe-fix boundary |
Only version bump and CHANGELOG Unreleased entry are in fixes |
Reclassify everything else as flag-only |
| Version fix exactness |
Replacement value comes from manifest; matched line is version-labeled |
Do not edit |
| Changelog restraint |
Entry cites commit/file evidence and lands under ## [Unreleased] |
Do not edit |
| Post-edit check |
Re-read edited ranges; no unrelated prose changed |
Revert the edit and report flag-only |
| Residual flags |
All non-safe issues remain in the report with reason |
Completion blocked until listed |
Output Contract
Return both machine-readable and human-readable surfaces when possible:
{
"opCell": "correct",
"scope": "recent|all|before-pr",
"base": "origin/main",
"changedCode": [{"status":"M","path":"src/client.ts"}],
"relatedDocs": [{"doc":"README.md","line":42,"term":"createClient","referenceType":"symbol"}],
"fixesApplied": [{"type":"version-mismatch","file":"README.md","line":12,"before":"1.2.0","after":"1.3.0"}],
"flagged": [{"type":"stale-code-example","severity":"MEDIUM","file":"docs/api.md","line":88,"reasonFlagOnly":"example intent cannot be inferred safely"}]
}
Completion means the safe fixes are applied or explicitly unavailable, and all remaining drift is flagged. A clean report with no edits is valid only after the diff-to-doc mapping and taxonomy pass ran.
1---2name: sync-docs-63description: Detect documentation-vs-code drift from a git diff, apply only safe documentation corrections, and report all unsafe stale-reference issues with file:line evidence. Use when "sync docs", "update changelog", "find outdated docs", "stale code examples", "doc drift", or "docs out of date".4---5
6# Sync Docs — correct documentation against code reality
7
8`sync-docs` is a `correct` op-cell: restore the invariant that public docs, examples, versions, and changelog entries describe the current code. It starts from a diff, maps the changed code to coupled Markdown, applies only the safe subset, and flags every semantic drift item with evidence.
9
10Safe means text substitution with a mechanically known target: **version-number bump** and **CHANGELOG `## [Unreleased]` entry**. Everything else is flag-only until a human or implementer verifies the intended prose change.
11
12## When to Apply / NOT
13
14Apply when a branch changes public APIs, imports, CLI flags, config names, package versions, exported symbols, examples, or user-visible behavior and docs may lag.
15
16NOT apply for generated documentation refreshes, mass prose rewrites, API design review, release-note authorship from scratch, or docs whose source of truth is intentionally external. For those, report drift signals but do not edit.
17
18## Inputs
19
20- **Mode**: `report` (default) or `apply`. `apply` still edits only safe-fix issues.
21- **Scope**:
22 - `recent`: changed files from the current branch or last few commits.
23 - `before-pr`: branch diff against the PR base; use before publishing.
24 - `all`: scan all tracked code and docs; slower, useful after migrations.
25- **Base**: explicit base ref is preferred. If absent, resolve the default branch, then fall back to `HEAD~5` for `recent`.
26
27## Workflow
28
291. **Pick scope and base.** Refuse ambiguous review scope when the user expects PR readiness. Use these commands as the runnable recipe:
30
31 ```bash
32 # before-pr: whole branch against the remote default branch
33 remote_head=$(git symbolic-ref --short refs/remotes/origin/HEAD || printf origin/main)
34 base=${remote_head#origin/}
35 git diff --name-status "origin/$base"...HEAD
36
37 # recent: if the default-branch diff fails, run the fallback explicitly
38 git diff --name-status HEAD~5..HEAD
39
40 # all: tracked docs and code inventory, not a behavioral diff
41 git ls-files
42 ```
43
44 In ODIN tool mode, use `bash` only for the `git` commands; use `find`, `search`, `read`, `lsp`, `ast_grep`, and `edit` for everything else.
45
462. **Compute changed code.** Keep only source/config/package files that can change docs. Exclude pure docs, vendored/generated paths, lockfiles unless version docs mention package manager output, and deleted files that were never public.
47
48 Output shape:
49
50 ```json
51 {"status":"M|A|D|R","oldPath":"src/old.ts","path":"src/new.ts","basename":"client","modulePath":"src/new","kind":"source|manifest|config|cli"}
52 ```
53
543. **Extract coupling terms.** For each changed code file, derive:
55
56 - filename stem: `client`, `auth-server`.
57 - full path and path without extension: `src/auth/client.ts`, `src/auth/client`.
58 - import strings from the diff: `from "pkg/auth"`, `require("pkg/auth")`, dynamic imports.
59 - exported/public symbols from `codegraph_search` / `codegraph_explore` when indexed.
60 - fallback symbols from language syntax:
61
62 ```bash
63 ast-grep --pattern 'export function $NAME($$$)' --lang ts <file>
64 ast-grep --pattern 'export class $NAME { $$$ }' --lang ts <file>
65 ast-grep --pattern 'pub fn $NAME($$$)' --lang rust <file>
66 git grep -nE '^(export |pub |def |class |func )' -- <file>
67 ```
68
694. **Discover related docs.** Search only live doc surfaces first: `README.md`, `CHANGELOG.md`, `docs/**/*.md`, `*.md` at repo root. For each coupling term, search docs and record `doc`, `line`, `term`, `referenceType` (`filename`, `full-path`, `import`, `symbol`, `url-path`, `version`). Default ignore list lives in `references/doc-issues.md`.
70
71 Runnable fallback:
72
73 ```bash
74 git grep -n -- '*.md' README.md CHANGELOG.md docs/ -- '<term>'
75 git grep -nE 'from ["'"''][^"'"'']+["'"'']|require\(["'"''][^"'"'']+["'"'']\)' -- '*.md'
76 ```
77
78 ODIN tool equivalent: `search` each escaped term across `README.md`, `CHANGELOG.md`, `docs/**/*.md`, then `read` the surrounding lines.
79
805. **Classify issues.** Use the taxonomy in `references/doc-issues.md`. Preserve certainty:
81
82 - **HIGH**: manifest version mismatch with exact current version; deleted/renamed public export still documented with symbol proof; changed import path in fenced example where the old path no longer resolves.
83 - **MEDIUM**: stale code example that mentions a changed file/symbol but needs semantic review; undocumented public export after entry-point/internal filtering; docs describing codegraph-reported dead code.
84 - **LOW**: doc-drift from zero code-coupling or weak filename-only coupling; broad stale prose suspicion.
85
866. **Apply safe fixes only.** In `apply` mode:
87
88 - **Version bump**: replace stale semver strings in docs with the manifest version when the line clearly labels a version (`version`, package badge, install snippet with `@x.y.z`). Avoid broad numeric replacement.
89 - **CHANGELOG `## [Unreleased]` entry**: insert a minimal bullet under the existing section, or create the section at the top if absent. Use commit messages or changed-file summaries; do not invent product claims.
90
91 Do not auto-edit removed exports, import paths, examples, undocumented exports, dead-code docs, or doc-drift prose. Those require intent.
92
937. **Flag the rest.** Emit a compact report sorted by severity, then file path:
94
95 ```text
96 HIGH docs/api.md:42 removed-export `createClient` no current public symbol; changed in src/client.ts
97 MEDIUM README.md:88 stale-code-example imports old path `pkg/client`; verify replacement `pkg/auth/client`
98 LOW docs/legacy.md:? doc-drift zero code-coupling; no live filename/import/symbol references
99 ```
100
1018. **Return fix ledger.** For every edit, record `file`, `line`, `type`, `before`, `after`, and evidence source. For every flag-only item, record `reasonFlagOnly`.
102
103## Native Detection Recipes
104
105### Code graph first
106
107When the repo is indexed, use the codegraph MCP for symbol truth:
108
109- `codegraph_search`: locate changed public symbols by name.
110- `codegraph_callers` / `codegraph_callees`: determine whether docs describe dead wrappers or removed API surfaces.
111- `codegraph_impact`: judge blast radius before calling an export undocumented.
112- `codegraph_files`: confirm entry points and module layout.
113
114Fallback when not indexed:
115
116```bash
117git grep -nE 'export (function|class|const|let|var)|export \{|module\.exports|pub (fn|struct|enum|trait)|^def |^class ' -- ':!node_modules' ':!generated'
118ast-grep --pattern 'import $X from $Y' --lang ts src
119ast-grep --pattern 'from $M import $$$N' --lang python .
120```
121
122### Manifest versions
123
124Read manifests directly and compare against doc mentions:
125
126- JavaScript/TypeScript: `package.json` `version`.
127- Rust: `Cargo.toml` `package.version`.
128- Python: `pyproject.toml` `project.version`, fallback `setup.cfg` / `setup.py` only as MEDIUM.
129- Go: module version is usually tag-derived; do not auto-fix unless a manifest line gives an exact version.
130
131Search docs for labeled semver:
132
133```bash
134git grep -nE '(version|v|@)[[:space:]:="'"'']*[0-9]+\.[0-9]+\.[0-9]+' -- '*.md'
135```
136
137### CHANGELOG evidence
138
139Use commits only as evidence, not as prose authority:
140
141```bash
142git --no-pager log --oneline --no-merges "origin/$base"..HEAD
143git diff --name-only "origin/$base"...HEAD
144```
145
146If `CHANGELOG.md` has no `## [Unreleased]`, create one immediately below the title. If it exists, insert under an appropriate subsection only when the subsection already exists; otherwise add a plain bullet under `## [Unreleased]`.
147
148## Anti-patterns
149
150- **Regex-only confidence on public API removal**: require codegraph, LSP, ast-grep, or exact diff evidence before HIGH.
151- **Mass replacing version-looking numbers**: examples, ports, years, protocol versions, and dates are not package versions.
152- **Fixing examples by guess**: changed import path is usually flag-only unless the diff explicitly contains a one-to-one rename.
153- **Treating CHANGELOG as drift**: append-only history has intentionally low code coupling.
154- **Reporting generated/versioned docs as stale**: they are snapshots unless explicitly in scope.
155- **Inventing release notes**: changelog bullets must cite commits or changed files.
156
157## Validation Gates
158
159| Gate | Pass Criteria | Blocking |
160|---|---|---|
161| Scope resolved | Base ref, scope, changed files, and ignored paths are listed | Stop with exact missing base or empty diff |
162| Evidence attached | Every issue has `file:line` when a line exists, plus the changed code source | Do not report line-specific claims without a read/search hit |
163| Safe-fix boundary | Only version bump and CHANGELOG Unreleased entry are in `fixes` | Reclassify everything else as flag-only |
164| Version fix exactness | Replacement value comes from manifest; matched line is version-labeled | Do not edit |
165| Changelog restraint | Entry cites commit/file evidence and lands under `## [Unreleased]` | Do not edit |
166| Post-edit check | Re-read edited ranges; no unrelated prose changed | Revert the edit and report flag-only |
167| Residual flags | All non-safe issues remain in the report with reason | Completion blocked until listed |
168
169## Output Contract
170
171Return both machine-readable and human-readable surfaces when possible:
172
173```json
174{
175 "opCell": "correct",
176 "scope": "recent|all|before-pr",
177 "base": "origin/main",
178 "changedCode": [{"status":"M","path":"src/client.ts"}],
179 "relatedDocs": [{"doc":"README.md","line":42,"term":"createClient","referenceType":"symbol"}],
180 "fixesApplied": [{"type":"version-mismatch","file":"README.md","line":12,"before":"1.2.0","after":"1.3.0"}],
181 "flagged": [{"type":"stale-code-example","severity":"MEDIUM","file":"docs/api.md","line":88,"reasonFlagOnly":"example intent cannot be inferred safely"}]
182}
183```
184
185Completion means the safe fixes are applied or explicitly unavailable, and all remaining drift is flagged. A clean report with no edits is valid only after the diff-to-doc mapping and taxonomy pass ran.