Generative drafting lowered the cost of producing documentation, but it shifted the bottleneck to verification, and most doc reviews still read the whole document instead of the model's footprint. The community already articulated the promotion: AI made every developer a reviewer, while nobody built a benchmark for the reviewing loop itself. The practical fix is not a better prompt but a generation manifest that records, at write time, exactly which sections the model touched, so a human reviews only the diff against that manifest.
A manifest gate becomes essential exactly when drafting is free, because free draft capacity inflates the volume of prose a reviewer would otherwise consume. Cheap generation should change the workflow rather than the workload: the model writes more, and the human verifies less, with a machine-checkable definition of what "less" means. Git author metadata cannot provide that definition, since rebases, squashes, and cooperative commit conventions make authorship attribution unreliable below the commit level.
The contract file
The workflow begins with a small YAML contract that sits next to the docs and assigns every section one of three owners: draft, human, or verify-with-example. The contract is the source of truth for both the model and the CI gate, and it turns an abstract policy like "humans own security notes" into a concrete map. You commit it to the repository so the generator and the reviewer read the same file.
# docs/doc-contract.yaml
version: 1
sections:
quickstart:
owner: model-draft
source: examples/demo.py
verify: run
config-reference:
owner: human-own
source: null
verify: none
api-error-codes:
owner: verify-with-example
source: src/errors.py
verify: run
security-notes:
owner: human-own
source: null
verify: none
Each owner value changes the obligations of the generator, the CI gate, and the reviewer in a different way. The table below summarizes the contract semantics that the rest of the pipeline enforces.
| Owner value | Who drafts | CI behavior | Human action |
|---|---|---|---|
model-draft |
the generator | allowed; section must appear in the manifest | read the diff, not the prose |
human-own |
nobody automated | any change outside the manifest fails the gate | author or approve in a separate PR |
verify-with-example |
the generator | requires a matching source hash | run the linked artifact on change |
The drafting step writes a manifest
Section ownership is a policy, and the manifest is its executable form. The manifest is written by the same process that produces the draft, not reconstructed later from git history, because only the generator knows exactly which headings it edited and which source files it read. This design separates the manifest from a risk score: a score has to be computed by someone who reads the document, whereas a manifest is free to compute because it is generated at draft time.
The drafting step in this workflow is built around MonkeyCode's free model access and its free server option; the free access removes the marginal cost of repeated drafts, and the free server option keeps the generation loop inside an environment you control rather than a shared endpoint. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The resulting artifact is a committed JSON file that lists every generated heading together with a hash of the source that fed that section.
{
"generator": "draft-loop",
"created_at": "2026-08-29T10:00:00Z",
"sections": {
"quickstart": {
"source_sha256": "7f2a9c1b4e0d8a3f6c5e2b1d9a4c8f0e6b7d5a2c1f9e8b0d3c4a5f6e7d8b9c0a1f"
},
"api-error-codes": {
"source_sha256": "3d8b6f2a91c4e5d0b7a6f3c2e1d9b8a7c6f5e4d3b2a1c9f8e7d6b5a4c3f2e1d0b9"
}
}
}
Set the generator field to the exact tool and version you used, because the manifest doubles as an audit log for the docs pipeline. The important detail is timing: the manifest is written in the same step as the draft, so there is no post-hoc guessing about what changed.
The gate: docbound.py
The enforcement layer is a single Python script with three subcommands: gate, diff, and verify. The gate rejects a manifest that lists a human-owned section, the diff compares two versions of a document and fails when a human-owned section changed, and the verify step checks source hashes from the manifest against the current files.
#!/usr/bin/env python3
"""docbound.py — gate model-drafted docs against an ownership contract."""
import argparse
import hashlib
import json
import re
import sys
from pathlib import Path
import yaml
CONTRACT_PATH = "docs/doc-contract.yaml"
MANIFEST_PATH = "docs/gen-manifest.json"
def extract_sections(markdown_text: str) -> dict:
"""Map each h2-h4 heading to the raw text of its section."""
lines = markdown_text.splitlines()
headings = [i for i, line in enumerate(lines) if re.match(r"^#{2,4}\s+", line)]
sections = {}
for idx, start in enumerate(headings):
end = headings[idx + 1] if idx + 1 < len(headings) else len(lines)
title = re.sub(r"^#{2,4}\s+", "", lines[start]).strip()
sections[title] = "\n".join(lines[start:end])
return sections
def changed_sections(base_md: str, changed_md: str) -> list:
base = extract_sections(base_md)
changed = extract_sections(changed_md)
touched = [t for t in base if t not in changed or base[t] != changed[t]]
touched += [t for t in changed if t not in base]
return touched
def load_contract(path: str) -> dict:
return yaml.safe_load(Path(path).read_text())
def load_manifest(path: str) -> dict:
return json.loads(Path(path).read_text())
def cmd_gate(args) -> None:
contract = load_contract(args.contract)
manifest = load_manifest(args.manifest)
errors = []
for heading in manifest["sections"]:
if heading not in contract["sections"]:
errors.append(f"{heading}: missing from contract")
elif contract["sections"][heading]["owner"] == "human-own":
errors.append(f"{heading}: human-owned section in model manifest")
if errors:
print("gate failed")
for err in errors:
print(" -", err)
sys.exit(1)
print("gate passed: manifest touches no human-owned sections")
def cmd_diff(args) -> None:
contract = load_contract(args.contract)
touched = changed_sections(
Path(args.base).read_text(), Path(args.changed).read_text()
)
issues = [
t for t in touched
if t in contract["sections"]
and contract["sections"][t]["owner"] == "human-own"
]
if issues:
print("human review required on:")
for heading in issues:
print(" -", heading)
sys.exit(1)
print("diff clean:", ", ".join(touched) if touched else "no sections changed")
def cmd_verify(args) -> None:
contract = load_contract(args.contract)
manifest = load_manifest(args.manifest)
for heading, meta in manifest["sections"].items():
entry = contract["sections"].get(heading)
if not entry or entry.get("verify") != "run":
continue
source = Path(entry["source"])
actual = hashlib.sha256(source.read_bytes()).hexdigest()
if actual != meta.get("source_sha256"):
print(f"{heading}: source drifted since the draft was generated")
sys.exit(1)
print("verify passed: example sources match the generation inputs")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
subs = parser.add_subparsers(dest="command", required=True)
gate = subs.add_parser(
"gate", help="reject manifests that touch human-owned sections"
)
gate.add_argument("--contract", default=CONTRACT_PATH)
gate.add_argument("--manifest", default=MANIFEST_PATH)
gate.set_defaults(func=cmd_gate)
diff = subs.add_parser(
"diff", help="list and gate sections changed between two doc versions"
)
diff.add_argument("--base", required=True)
diff.add_argument("--changed", required=True)
diff.add_argument("--contract", default=CONTRACT_PATH)
diff.set_defaults(func=cmd_diff)
verify = subs.add_parser(
"verify", help="confirm example sources have not drifted"
)
verify.add_argument("--contract", default=CONTRACT_PATH)
verify.add_argument("--manifest", default=MANIFEST_PATH)
verify.set_defaults(func=cmd_verify)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
Run the three checks locally before opening a PR, in the same order the CI will use them. The diff command needs a base version of the document and the changed version, which works with any git checkout.
python scripts/docbound.py gate
python scripts/docbound.py diff --base /tmp/docs-base.md --changed docs/index.md
python scripts/docbound.py verify
For GitHub Actions, the workflow below is a starting point that fetches the base document from the target branch. It runs only when docs or the gate script itself change, so it costs nothing on unrelated PRs.
name: docs-gate
on:
pull_request:
paths: ["docs/**", "scripts/docbound.py"]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pyyaml
- run: python scripts/docbound.py gate
- run: |
git show origin/main:docs/index.md > /tmp/docs-base.md
python scripts/docbound.py diff --base /tmp/docs-base.md --changed docs/index.md
- run: python scripts/docbound.py verify
What changes in practice
A manifest gate changes the review meeting because reviewers no longer open the document at the top; they open a short list of touched headings and the manifest that explains why those headings changed. Expect the gate to print two or three headings on a typical docs PR, and reserve slow reading for human-owned sections that actually appear in that list. That reduction is the measurable outcome, and it is why the manifest belongs in the repository rather than in a conversation summary.
Limitations and teams that should skip this
The gate trusts the manifest as an honest record, so a generator that edits files outside its declared output, or a teammate who pastes model text into a human-owned PR, bypasses every check; code owners on human-owned paths are the compensating control. Heading-based section extraction breaks on duplicate headings and heavily fragmented tables, so sections need stable anchor slugs for reliable diffing. The gate narrows what humans read but cannot judge whether draft-tier prose is correct, because correctness still comes from executable examples and tests outside the docs. Teams without a named owner for human sections should not enable free-tier drafting at all, since unowned prose will accumulate faster than review capacity, and a two-page README for a stable product does not justify the contract overhead.
A measurement to start with
If your documentation already has an ownership contract, add the manifest and the docbound gate, then measure one number: how many sections reviewers actually open across the next ten docs PRs. If that number stays flat, the contract was never enforced tightly enough to matter. If it drops to a handful of headings, free drafting becomes an asset rather than a liability, because the review loop finally scales with the diff instead of the document.
Top comments (0)