Cheaper drafting does not make documentation cheaper; it moves the bill from writing minutes to review minutes. The practical stop rule is therefore economic rather than stylistic: measure what reviewing a section costs relative to writing it by hand, and let that ratio decide which sections a model may draft and which stay human-owned. This post gives the ledger schema, a small runnable script that assigns draft rights, a unit test for the rule, a CI gate for the whole page, and the cases where you should not adopt any of it. Every number in the fixture below is synthetic and labeled as such, so you can drop in your own review data without inheriting my assumptions.
Why review time is the honest budget
When a draft costs nothing, the only scarce resource left in the loop is a reviewer's attention, and attention is spent per claim rather than per page. A 900-word concept page with two contested statements can consume more senior time than a 200-word reference stub with none, which means word count is the wrong denominator for planning. Review minutes per section survive tooling changes, because they are measured from artifacts you already produce: comments, resolved threads, and the edits that changed a factual statement rather than a comma.
The rule I use has three outcomes instead of two, because "model drafts it" and "human owns it" are not the only options on a real docs team. A section can be freely drafted, drafted behind a named checkpoint, or reserved for a human author from the first sentence. The thresholds below are starting values, and you should treat them as calibration targets rather than universal constants.
The ledger: four numbers per section
Each row describes one reviewable unit, and a unit should be a section with a single owner and a single topic. words is the rendered length, review_minutes is time spent on that section in review, and blockers counts comments that changed a factual claim. human_minutes is the measured time to write the same section by hand, and it is the anchor that keeps the ratio meaningful across teams.
{
"page": "docs/concepts/queue.md",
"captured_at": "2026-10-24",
"sections": [
{"path": "overview", "words": 320, "review_minutes": 9.0, "blockers": 0, "human_minutes": 75.0},
{"path": "delivery", "words": 410, "review_minutes": 22.0, "blockers": 1, "human_minutes": 80.0},
{"path": "limits", "words": 180, "review_minutes": 34.0, "blockers": 3, "human_minutes": 55.0},
{"path": "migration", "words": 260, "review_minutes": 41.0, "blockers": 2, "human_minutes": 70.0}
]
}
Those four rows are invented to make the rule visible; replace them with exported review timings before you believe any verdict. The interesting row is limits, which is the shortest section and the most expensive to review, because short sections about behavior attract exactly the claims nobody can verify quickly.
Step 1: capture the baseline once per page
You need human_minutes only once per section, and a timeboxed reconstruction is good enough for a first pass. Ask an engineer to write that section on a throwaway branch from the same sources, stop at the same level of detail, and record the elapsed minutes in the ledger with the capture date. Store the date because baselines decay when the subject matter changes, and a stale baseline silently inflates or deflates every verdict downstream.
Step 2: instrument review minutes per section
Most review tools already timestamp comments, so approximate attribution is cheap: assign each comment thread to the section it touches and sum the gap between the first and last human reply. Exclude bot comments, exclude pure formatting threads, and keep a separate blockers count for threads that changed a claim. If your tooling cannot attribute a thread, put it in an unattributed bucket instead of guessing, and report that bucket in the output so the ratio never looks more precise than it is.
Step 3: assign draft rights with a script
The artifact below is standard-library Python, keeps thresholds in named constants, and returns a machine-readable verdict per section plus a page-level ratio. It is deliberately small so a reviewer can read the whole rule in one sitting.
# draftcost.py — assign draft rights per docs section from review cost
from __future__ import annotations
import json
import sys
from dataclasses import dataclass
from pathlib import Path
BUDGET_RATIO = 0.40 # page review minutes / page hand-written minutes
FREE_RATIO = 0.15 # section may be drafted without a checkpoint
CHECKPOINT_RATIO = 0.50 # above this, a human writes the section
BLOCKER_HARD_STOP = 2
@dataclass(frozen=True)
class Section:
path: str
words: int
review_minutes: float
blockers: int
human_minutes: float
def per_100w(words: int, minutes: float) -> float:
if words <= 0:
raise ValueError(f"words must be > 0, got {words}")
if minutes < 0:
raise ValueError(f"minutes must be >= 0, got {minutes}")
return minutes / words * 100.0
def assign(section: Section) -> str:
if section.human_minutes <= 0:
raise ValueError(f"{section.path}: human_minutes must be > 0")
ratio = section.review_minutes / section.human_minutes
if section.blockers >= BLOCKER_HARD_STOP or ratio > CHECKPOINT_RATIO:
return "human_owned"
if section.blockers >= 1 or ratio > FREE_RATIO:
return "model_with_checkpoint"
return "model_free"
def load(path: Path) -> list[Section]:
raw = json.loads(path.read_text(encoding="utf-8"))
return [Section(**row) for row in raw["sections"]]
def main(argv: list[str]) -> int:
ledger_path = Path(argv[1])
sections = load(ledger_path)
rows = [
{
"path": s.path,
"review_per_100w": round(per_100w(s.words, s.review_minutes), 2),
"ratio": round(s.review_minutes / s.human_minutes, 3),
"blockers": s.blockers,
"verdict": assign(s),
}
for s in sections
]
total_review = sum(s.review_minutes for s in sections)
total_human = sum(s.human_minutes for s in sections)
page_ratio = total_review / total_human
print(json.dumps(
{"sections": rows, "page_ratio": round(page_ratio, 3), "budget": BUDGET_RATIO},
indent=2,
))
return 0 if page_ratio <= BUDGET_RATIO else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Run it against the fixture and check the exit code, because the exit code is the part CI reads.
python draftcost.py ledger.json; echo "exit=$?"
With the synthetic fixture the page ratio exceeds the 0.40 budget, so the script exits non-zero and the page fails its gate. That is the intended behavior: the rule is supposed to tell you that this particular page is cheaper to write by hand than to review.
Step 4: pin the rule with tests
A rule that lives only in CI configuration drifts, so keep two assertions that encode the intent of each boundary. These tests use only unittest and run in milliseconds, which makes them safe to keep in the docs repository itself.
# test_draftcost.py
import unittest
from draftcost import Section, assign, per_100w
class DraftCostTest(unittest.TestCase):
def test_zero_words_is_rejected(self):
with self.assertRaises(ValueError):
per_100w(0, 12.0)
def test_low_cost_no_blockers_is_model_free(self):
s = Section("overview", 320, 4.0, 0, 75.0)
self.assertEqual(assign(s), "model_free")
def test_blockers_force_human_ownership(self):
s = Section("limits", 180, 6.0, 3, 55.0)
self.assertEqual(assign(s), "human_owned")
def test_expensive_section_is_human_owned(self):
s = Section("migration", 260, 41.0, 0, 70.0)
self.assertEqual(assign(s), "human_owned")
if __name__ == "__main__":
unittest.main()
python -m unittest -v test_draftcost.py
The decision table you can argue about
| Section profile | Review ratio | Blockers | Verdict | Who signs off |
|---|---|---|---|---|
| Restates config names, links a test | <= 0.15 | 0 | model_free |
Any reviewer on rotation |
| Explains behavior with one contested word | 0.15–0.50 | 1 | model_with_checkpoint |
Named section owner |
| Anything about limits, security, or timing | any | >= 2 | human_owned |
Human writes from scratch |
| Long but mechanical reference | <= 0.15 | 0 | model_free |
Any reviewer on rotation |
| Migration steps | any | >= 1 | human_owned |
Human writes from scratch |
What the model may draft, and what a human owns
The table is only useful if it maps onto content types, because nobody assigns draft rights to a ratio in the abstract. A model may draft restatements of values that already exist in source, prose that describes a signature with a linked definition, and explanations of an error whose message is produced by a test. A model may draft under checkpoint the sections that describe observable behavior but depend on one team-specific term.
A human must own every statement about safety, defaults, deprecation timelines, supported versions, data handling, and migration order. Those claims fail quietly and expensively, and their review cost is high precisely because they cannot be validated from the repository alone. The ratio is a detector for that failure mode, not a substitute for judgment about which category a sentence belongs to.
Where free model access and a free server actually fit
If drafting runs on free model access, the generator can be a disposable job rather than a permanent resident, which changes the security posture more than it changes the cost. The free server option is useful for exactly that shape of work: a short-lived container that checks out the repository, drafts the sections marked model_free, and exits without holding long-lived credentials. That least-privilege argument is the real reason I would run the draft step remotely instead of on a laptop with a personal token. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Treat the availability of free model access and a free server as operator-supplied and time-sensitive rather than guaranteed; verify current terms before you put either behind an automated job. Do not send confidential drafts, unpublished security notes, or customer-specific content to any free endpoint without an explicit review of the terms in force that day. The workflow above degrades gracefully to local models, because the only interface it needs is "given a section outline and registered sources, return prose."
Limitations and who should not use this
The measurement overhead can exceed the savings on small docs sets, so a team publishing a handful of pages per quarter should skip the ledger and review everything by hand. Teams without timestamped review data should not fabricate review_minutes, because invented inputs make the verdicts look authoritative while being arbitrary. Pages whose sections cannot be attributed to a single owner will produce noisy ratios, and pages with volatile subject matter need their human_minutes baseline refreshed on a schedule.
Finally, the rule optimizes for predictable review cost, not for documentation quality, and those goals diverge whenever a page is strategically important but cheap to review. Use the ratio to allocate attention and to escalate contested sections, then keep a human decision on the pages that carry the most risk. If you try the ledger on one page this week, start by recording blockers honestly, since that single integer changes more verdicts than any threshold you tune.
Top comments (0)