The core problem with model-drafted documentation is not that the sentences are wrong, but that nobody can tell which sentences someone has actually vouched for. A redrawn section can be 95% accurate and still ship a changed rate limit, a dropped prerequisite, or a security claim that no human ever read. The workflow below fixes the accountability gap instead of the wording: it classifies every claim in a doc, allows the model to draft only the low-risk classes, and requires a named human signature on the rest before the build turns green.
The artifact is a claim contract: a small YAML ledger beside your docs plus a ~90-line Python checker. It runs in under a second, needs no API key, and produces the same pass/fail in a local shell, a pre-commit hook, or a background job. I present it as a template to adapt rather than a finished product; run it against your own repository before trusting any of the checks.
1. Name the failure before choosing a tool
Fact-checking tools usually compare a new draft against the old draft, which means they inherit the old draft's mistakes. If the previous version already carried an outdated timeout value, a diff-based gate stays perfectly happy while both versions stay wrong. Accountability is a different axis: it asks who put their name on the claim, not whether the text changed.
That distinction matters because most doc drift comes from ordinary refactors. A constant gets renamed, a default flips, a flag disappears, and the prose keeps describing the world as it was. No model needs to be involved for that to happen. The gate should therefore anchor each claim to something executable or greppable in the repo, and separately record whether a human accepted the sentence.
2. Split claim classes before you prompt anything
Treat this table as the contract. Anything in the left column is prose the model may draft freely; anything in the right column needs a human signer or the build fails.
| Claim kind | Example | Model may draft | Human signature required |
|---|---|---|---|
narrative |
"This section explains the retry model." | yes | no |
example |
Fenced snippet with --dry-run
|
yes, must execute | no |
glossary |
Definition of a term already in the code | yes | no |
prereq |
"Requires Python 3.11 or newer" | no | yes |
limit |
Timeout, page size, concurrency cap | no | yes |
security |
Auth flow, token lifetime, scopes | no | yes |
cost |
Anything about billing or quotas | no | yes |
guarantee |
"Safe to call twice" | no | yes |
The right column rarely changes, which is the point: you decide the policy once and then apply it mechanically. When someone wants a new class, the review conversation is about the table, not about a specific paragraph.
3. Write the ledger, one claim per row
# docs/claims.yaml
claims:
- id: retry-timeout
kind: limit
section: "Retry behavior"
anchor: "src/retry.py#DEFAULT_TIMEOUT_MS"
signed_by: avery
drafted_by: model
- id: python-floor
kind: prereq
section: "Installation"
anchor: "pyproject.toml#requires-python"
signed_by: avery
drafted_by: model
- id: retry-overview
kind: narrative
section: "Retry behavior"
anchor: "src/retry.py#def retry"
drafted_by: model
The anchor field is deliberately dull: a repo-relative path plus a literal token that must appear in that file. It is not a schema, not an AST query, and not a semantic match. Boring anchors survive refactors better than clever ones, and a broken anchor is exactly the signal you want.
The signed_by value must appear in docs/owners.txt, a plain list of handles. Keeping the allowlist in a file rather than in the script means the diff that grants signing rights is reviewable like any other change.
4. Run the checker in four steps
- Save the script below as
tools/docgate.pyand install PyYAML (python -m pip install pyyaml). - Create
docs/owners.txtwith one handle per line, starting with your own. - Run
python tools/docgate.py docs/claims.yaml --repo .and expect exit code 0 on a clean tree. - Wire it into a pre-commit hook or a CI job once the ledger is complete, never before.
#!/usr/bin/env python3
"""docgate.py: fail when a doc claim is unsigned, unanchored, or orphaned."""
import argparse, sys
from pathlib import Path
import yaml
HUMAN_REQUIRED = {"prereq", "limit", "security", "cost", "guarantee"}
MODEL_ALLOWED = {"narrative", "example", "glossary"}
def anchor_ok(repo: Path, anchor: str):
path, _, token = anchor.partition("#")
target = repo / path
if not target.is_file():
return False, f"missing file {path}"
text = target.read_text(encoding="utf-8", errors="replace")
if token and token not in text:
return False, f"token {token!r} not found in {path}"
return True, "ok"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("ledger")
ap.add_argument("--repo", default=".")
ap.add_argument("--owners", default="docs/owners.txt")
args = ap.parse_args()
repo = Path(args.repo)
owners = {l.strip() for l in Path(args.owners).read_text().splitlines() if l.strip()}
claims = yaml.safe_load(Path(args.ledger).read_text())["claims"]
seen, errors = set(), []
for c in claims:
cid, kind = c.get("id"), c.get("kind")
if not cid or cid in seen:
errors.append(f"duplicate or missing id: {cid!r}")
continue
seen.add(cid)
ok, why = anchor_ok(repo, c.get("anchor", ""))
if not ok:
errors.append(f"{cid}: stale anchor -> {why}")
if kind in HUMAN_REQUIRED:
signer = c.get("signed_by")
if not signer:
errors.append(f"{cid}: kind={kind} requires signed_by")
elif signer not in owners:
errors.append(f"{cid}: signer {signer!r} absent from {args.owners}")
elif kind not in MODEL_ALLOWED:
errors.append(f"{cid}: unknown kind {kind!r}")
for e in errors:
print(f"FAIL {e}")
print(f"{len(claims) - len(errors)}/{len(claims)} claims cleared")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
The output shape below is illustrative and reflects the sample ledger, so edit anchors to match your own files before reading anything into it:
FAIL retry-timeout: stale anchor -> token 'DEFAULT_TIMEOUT_MS' not found in src/retry.py
FAIL python-floor: signer 'avery' absent from docs/owners.txt
2/3 claims cleared
That first line is the useful one. A renamed constant invalidates the sentence that quoted it, and the gate says so in the same breath as the refactor.
5. Let the model draft against the contract, not against the repo
Once the ledger exists, the drafting loop gets narrow on purpose. For each claim, hand the model the section heading, the claim kind, and the anchored source excerpt, then ask for prose that says nothing the excerpt cannot support. The model never sees the whole repository, because a model with full context will happily invent a timeout it never read.
This is where MonkeyCode's free model access fits the workflow: drafting a dozen claim-scoped paragraphs does not warrant a paid pipeline, and the drafting step is the part you want to repeat cheaply. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free server option is similarly modest in role: run docgate.py there as a scheduled job so a draft branch is checked before any reviewer opens it. Treat both as operator-supplied availability claims and confirm current terms yourself rather than relying on this article.
Keep the ordering strict. The model drafts, the human signs, the gate verifies the signature and the anchor. If you invert the order and let the gate run only after a human has read the page, you have built a spell-checker with extra steps.
6. Limitations and who should skip this
- A literal-token anchor proves the symbol still exists, not that its value is unchanged. Pair it with a value assertion for anything numeric that matters.
- Two-person teams will find the signature ritual heavy. Below roughly ten pages of API docs, a checklist in the pull request template is cheaper.
- This does nothing for prose quality. Model-drafted narrative still sounds like model-drafted narrative, and the gate will pass it happily.
- Generated reference docs from docstrings should skip the ledger entirely; the source already owns those claims.
7. What to measure after two weeks
Count three things: claims whose anchors broke, claims that failed signature after a model draft, and claims a human rewrote substantially. The first number tracks repo churn, the second tracks prompt quality, and the third tells you which claim kinds you classified too optimistically. If the third number stays high for a class like limit, move that class into the human-only column and stop paying for the round trip.
Start with one page and eight claims. If the ledger cannot survive a week of ordinary merges, the anchors were too clever; if it survives untouched, the review step has already stopped happening.
Top comments (0)