Drafting Rights: Assign Every Doc Section to a Model or a Named Human
Most documentation pipelines fail on accountability rather than prose quality, because nobody can point at a sentence and name the human who approved it. That gap widens the moment a model drafts pages: reviewers read fl
Most documentation pipelines fail on accountability rather than prose quality, because nobody can point at a sentence and name the human who approved it. That gap widens the moment a model drafts pages: reviewers read fluently, approve quickly, and silently inherit claims they never verified. The rule I now apply is narrow enough to enforce in CI — a model may draft anything that describes how the software behaves today, and nothing that promises what it will do tomorrow.
My earlier gates checked numerals, executed fenced examples, and measured fact fidelity against a source blob. All of those answer "is this sentence true?" and none of them answer "was a model even allowed to write it?". This article is about the second question, expressed as a checked artifact instead of a review guideline.
Step 1: Split documentation into four rights tiers
The useful distinction is not by page or by product area, but by what a sentence commits the team to. Descriptive content ages on its own; commitment content ages into an incident. I classify every section into one of four tiers before any drafting happens.
| Tier | Typical content | Model may draft | Human must own |
|---|---|---|---|
prose |
intros, transitions, glossary wording, navigation copy | yes | nobody, but it still passes the fact gates |
mechanism |
how the current build behaves, config keys, CLI flags, error meanings | yes, from an evidence blob | a named reviewer confirms the evidence pointer |
commitment |
support windows, deprecation dates, pricing, SLAs, roadmap wording | no | a named owner with a review date |
obligation |
security posture, data retention, licensing, legal terms | no | a named owner plus the team carrying the risk |
The two lower tiers are where models earn their keep, and the two upper tiers are where most bad generated docs come from. A model that writes "retries are capped at three attempts" is describing code; a model that writes "we support this until 2028" is inventing a promise.
Step 2: Record the tiers in a manifest, not in a wiki
A tier decision that lives in a review comment is gone in two sprints. I keep it in docs-rights.yml at the repository root, with per-file section overrides and a conservative default. Sections are matched by a slug derived from the ## heading, so the manifest reads like the page it governs.
# docs-rights.yml
version: 1
defaults:
tier: mechanism # unlisted sections are treated as behavior, not promise
files:
docs/quickstart.md:
sections:
"install": prose
"first-run": mechanism
"support-window": commitment
docs/security.md:
sections:
"data-retention": obligation
"reporting-a-vulnerability": obligation
Two defaults matter here. Unlisted sections inherit mechanism rather than prose, so silence never grants the model more freedom than intended. And the manifest is reviewed like code, which means widening a model's rights shows up as a diff someone can object to.
Step 3: Enforce it with a linter and a re-review receipt
The manifest alone is documentation about documentation, so it needs an executor. The script below parses headings, hashes each normalized section body, and compares that hash with a receipt written at review time. When the reviewed text changes, the receipt goes stale and the check fails until a human looks again.
# tools/docs_rights.py (reference implementation; run it on a scratch branch first)
from __future__ import annotations
import hashlib, json, re, sys
from pathlib import Path
import yaml
HEADING = re.compile(r"^##\\s+(.+?)\\s*$", re.M)
HUMAN_TIERS = {"commitment", "obligation"}
MODEL_TIERS = {"prose", "mechanism"}
def sections(md: str) -> dict[str, str]:
marks = list(HEADING.finditer(md))
out: dict[str, str] = {}
for i, m in enumerate(marks):
end = marks[i + 1].start() if i + 1 < len(marks) else len(md)
slug = re.sub(r"[^a-z0-9]+", "-", m.group(1).lower()).strip("-")
out[slug] = md[m.end():end].strip()
return out
def digest(text: str) -> str:
return hashlib.sha256(re.sub(r"\\s+", " ", text).strip().encode()).hexdigest()[:16]
def check(root: Path) -> int:
cfg = yaml.safe_load((root / "docs-rights.yml").read_text())
errors: list[str] = []
for rel, spec in cfg["files"].items():
path = root / rel
receipt_file = root / "docs-receipts" / f"{path.stem}.json"
receipt = json.loads(receipt_file.read_text()) if receipt_file.exists() else {}
for slug, body in sections(path.read_text()).items():
tier = spec.get("sections", {}).get(slug, cfg["defaults"]["tier"])
entry = receipt.get(slug, {})
if tier in HUMAN_TIERS:
if not entry.get("owner"):
errors.append(f"{rel}#{slug}: tier {tier} needs a named human owner")
elif entry.get("section_sha") != digest(body):
errors.append(f"{rel}#{slug}: human-tier text changed, re-review required")
elif not entry.get("evidence"):
errors.append(f"{rel}#{slug}: missing evidence pointer")
elif tier in MODEL_TIERS and entry and entry["section_sha"] != digest(body):
errors.append(f"{rel}#{slug}: drifted after review, receipt invalidated")
for e in errors:
print(e)
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(check(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
A receipt is deliberately boring, because boring receipts get filled in. It records the owner, the date, the evidence pointer, and the section hash that was actually read.
{
"support-window": {
"owner": "platform-docs",
"reviewed_at": "2026-11-06",
"evidence": "release-calendar.md#lts-policy",
"section_sha": "4f9c1a7b2e0d3856"
}
}
Wire it into whatever blocks merges today: python tools/docs_rights.py . as a CI step, or the same command in a pre-commit hook for faster feedback. The exit code is the contract, so the surrounding tooling does not matter.
Step 4: Escalate tiers by signal, not by taste
Reviewers disagree about wording far more than about risk, so the escalation rule should be mechanical. I scan section text for tokens that imply a promise or an obligation and raise the tier automatically, then let a human argue it back down with a manifest override.
SIGNALS = {
"commitment": r"\\b(we support|supported until|guarantee|SLA|deprecat\\w*|end of life|pricing)\\b",
"obligation": r"\\b(GDPR|SOC ?2|HIPAA|retention|encrypt\\w*|liabilit\\w*|terms of service)\\b",
}
A single match is enough to move a section up a tier, and the linter reports the matched phrase so the override is auditable. False positives cost one line of YAML; false negatives cost a support thread six months later. If your team argues about the signal list, that argument is the most valuable part of this step.
Step 5: Draft only the permitted sections, from an evidence blob
Generation becomes simple once the tiers exist: the drafting job reads the manifest, filters to prose and mechanism sections, and exits before calling any model when a page is entirely human-owned. The prompt then carries a fact blob produced by code, plus an explicit refusal rule for anything that looks like a promise.
ROLE: draft only the sections listed under TARGETS.
EVIDENCE: facts.json (generated from source, read-only)
RULES:
- no numbers, dates, or durations that are absent from EVIDENCE
- no support windows, pricing, or deprecation promises; emit {{HUMAN_OWNED}} instead
- keep every {{placeholder}} verbatim
OUTPUT: markdown, one "## heading" per target section, nothing else
On a refusal the job writes {{HUMAN_OWNED}} into the draft and opens a checklist item for the tier owner, which keeps the pipeline honest without blocking the rest of the page. I run this drafting pass on MonkeyCode's free server with its free model access, which suits batch jobs that are cheap, interruptible, and re-run from scratch on every doc change. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The important detail is that the free-tier convenience does not touch the tier system: the refusal path is decided by the manifest before a request is made, not by the model's judgment during one. If you swap in another provider, the workflow stays identical and only the request code changes.
Step 6: Track the human-authored share as a release metric
Once rights are assigned, you can measure something most doc pipelines ignore: how much of the documentation changed this release was actually written by a person. Compute it from the diff rather than from impressions, counting changed lines per tier and dividing human-tier lines by total changed doc lines.
git diff --numstat origin/main -- 'docs/**/*.md' | awk '{added += $1} END {print added " added doc lines"}'
A dropping share is not automatically wrong, but an unexplained drop usually means a commitment section quietly became model-drafted prose. Publish the number next to the release notes for three or four cycles and the conversation about documentation quality changes tone. Treat the first values as a baseline, not as a target to optimize.
Limitations and who should skip this
The manifest rots if nobody reviews tier overrides, and slug collisions on duplicate headings will silently merge two sections into one check. The hash comparison is whitespace-normalized but still breaks on reformatting, so expect a few false re-review requests after a formatting pass. And this workflow does not replace the earlier fact checks: a mechanism section can be perfectly rights-compliant and still be wrong.
Skip it if you maintain a small repository where the same person writes and ships every page, since the receipt overhead buys nothing there. Skip it too if you cannot name an owner for your security or pricing documentation, because that is the real prerequisite and the linter will only keep telling you so. Mechanically generated API references are also out of scope: no model prose means no drafting rights to assign.
What to do on Monday
Start with one page that mixes behavior and promises, classify its sections, and run the linter before you generate anything. The first run usually reveals two or three commitment sentences living inside a tutorial, which is exactly the content that should never have been model-drafted. If you build a tier table of your own, I would rather read yours than defend mine, since the boundaries are where the useful disagreement lives.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.