The expensive question in model-drafted documentation is not whether the prose reads well; it is which paragraphs a named human is accountable for after merge. If you cannot answer that from the pull request alone, review turns into archaeology and nobody signs anything. This workflow answers it mechanically: three lanes, a lane map keyed by section, and a diff check that refuses to let a model touch accountability-bearing text before a reviewer opens the page.

The failure this fixes

Consider an illustrative scenario rather than a reported incident: a nightly job drafts prose for docs/install.md, and its diff quietly rewrites the sentence describing which platforms are supported. The prose is grammatical, the CI is green, and the reviewer skims forty changed lines because the title says "docs cleanup". Two weeks later support learns the page promises something the code does not, and blame lands on whoever merged it. Nothing here happened to me; it is the shape of the failure, not a record of one.

The root cause is not model quality, because better prose produces the same ambiguity. The root cause is that a doc file has one review state, while its paragraphs have very different accountability requirements. A tagline and a supported-platforms statement arrive in the same diff and get the same scrutiny. Fix the granularity first and the prose quality question becomes secondary.

Three lanes, one decision each

Every section gets exactly one lane, and the lane decides who may produce text and what the reviewer must check.

Lane Model may produce text Required in the diff Reviewer checks
draft Yes, freely Nothing beyond normal formatting Accuracy of tone and examples only
cited Yes, with a source per claim block A line starting with Source: pointing at a repo path That the cited file actually says it
human No, placeholder only A named owner and a review comment The whole statement, from scratch

The useful property is that the lane map is a small reviewable file, so arguments about ownership happen once, in a pull request about the map, and not repeatedly inside prose diffs. The lanes are not a quality ranking; human is not better than draft, it simply carries different consequences.

Step 1: Write the lane map

The map lives beside the docs and is keyed by <file>#<section heading>, using the leaf heading text so keys stay short and readable.

# docs/ownership.yaml version: 1 budget: draft_lane_added_lines: 120 sections: "docs/install.md#Prerequisites": lane: cited owner: "@avery"  "docs/install.md#Supported platforms": lane: human owner: "@avery"  "docs/install.md#Why this exists": lane: draft owner: "@avery" 

Start by laning only the file you change most often, because a partial map with a hard failure on unmapped sections is more useful than a complete map nobody finished. Unmapped sections fail closed, which forces the map to grow exactly where diffs actually land. The owner field is documentation for humans; the checker only enforces lane, so do not treat ownership as automated.

Step 2: Classify added lines by section

The checker reads the diff at zero context, maps each added line to the nearest preceding heading in the new file, then groups by section. The script below is illustrative and has not been executed as part of this article.

#!/usr/bin/env python3 """Illustrative lane checker. Not executed or tested here.""" import re, subprocess, yaml HEADING = re.compile(r"^(#{1,6})\s+(.*)$") def sections_of(path): out, stack = [], [] with open(path, encoding="utf-8") as fh: for i, line in enumerate(fh, start=1): m = HEADING.match(line.rstrip("\n")) if m: stack = stack[: len(m.group(1)) - 1] + [m.group(2).strip()] out.append((i, " > ".join(stack))) return out def section_for(sections, lineno): current = "(top)" for start, path in sections: if start <= lineno: current = path else: break return current def added_lines(base): diff = subprocess.run( ["git", "diff", "--unified=0", "--no-color", f"{base}...HEAD", "--", "docs/"], capture_output=True, text=True, check=True).stdout path, lineno, rows = None, 0, [] for line in diff.splitlines(): if line.startswith("+++ b/"): path = line[6:] elif line.startswith("@@"): lineno = int(re.search(r"\+(\d+)", line).group(1)) elif line.startswith("+") and path: rows.append((path, lineno, line[1:])) lineno += 1 return rows def main(base): ledger = yaml.safe_load(open("docs/ownership.yaml", encoding="utf-8")) lanes = ledger.get("sections", {}) budget = ledger.get("budget", {}).get("draft_lane_added_lines", 0) cache, groups = {}, {} for path, lineno, text in added_lines(base): if path not in cache: cache[path] = sections_of(path) heading = section_for(cache[path], lineno) key = f"{path}#{heading.split(' > ')[-1]}" groups.setdefault(key, []).append(text) problems, drafted = [], 0 for key, lines in groups.items(): entry = lanes.get(key) if entry is None: problems.append(f"{key}: section missing from lane map") elif entry["lane"] == "human": problems.append(f"{key}: {len(lines)} added line(s) in a human-owned section") elif entry["lane"] == "cited" and not re.search(r"^\s*source:", "\n".join(lines), re.I | re.M): problems.append(f"{key}: cited lane section has no line starting with `Source:`") else: drafted += len(lines) if budget and drafted > budget: problems.append(f"draft and cited lanes added {drafted} lines, budget is {budget}") for p in problems: print(f"lane-check: {p}") return 1 if problems else 0 if __name__ == "__main__": raise SystemExit(main("origin/main")) 

Run it locally against your branch base before pushing, so the first failure is yours to read rather than a reviewer's.

python scripts/lane_check.py origin/main 

Step 3: Wire it into CI with a budget

The check belongs in a job that fetches full history, because git diff base...HEAD needs a real merge base to be meaningful. A shallow checkout silently produces empty diffs, which fail open, which is worse than having no check at all.

name: doc-lanes on: [pull_request] jobs: lanes: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - run: pip install pyyaml - run: python scripts/lane_check.py "origin/${{ github.base_ref }}" 

The draft_lane_added_lines budget is a policy number, not a measured threshold, and you should pick it from your team's actual review capacity. Its job is to convert "this docs PR is too big to read carefully" into a failing check that asks for a split. Treat the initial value as a guess to be adjusted after two weeks of real diffs.

Step 4: Give humans a visible override

Some edits are legitimate in a human lane without a model: fixing a broken link, correcting a stale path, repairing a typo in a support matrix. Banning those outright pushes people to weaken the map instead, so provide one override that leaves a trace in history.

Lane-Override: docs/install.md#Supported platforms Lane-Override-Reason: fix dead link in existing sentence, no claim change 

The checker reads the commit message, prints the override to the job log, and passes the section. Because the trailer lives in the commit, an override is auditable later without asking anyone to remember the context. Keep the reason field mandatory in review culture even if the script does not parse it.

Which content belongs in which lane

Content type Lane Reason
Motivation, architecture narrative, tutorials draft Wrong prose is annoying, not costly
Configuration steps, flags, code samples cited Must trace back to source files
Support matrix, deprecation promises, security posture human Commitments a company makes
Pricing, quotas, licence and legal text human Regulated or contractual
Generated API references excluded Machine output, not hand-laned

Where the drafting lane actually runs

The drafting lane needs a model endpoint you can point a script at, and a place to run dry checks when CI minutes are contested. MonkeyCode's free model access and free server option are relevant there, since the lane checker and a draft script are small workloads, and evaluating a workflow should not require a procurement conversation. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Verify the current terms of any free offering directly before you build a pipeline around it, and keep the endpoint behind an environment variable so the pipeline stays portable.

Limitations and who should skip this

Provenance here is by convention, not cryptography: a model that commits directly to main with a human author field bypasses every lane, and only branch protection stops that. Heading-derived keys break when a heading is renamed, which is why the unmapped-section failure is deliberately loud rather than a warning. Duplicate leaf headings in one file collide on the same key, so either keep headings unique or extend the key to the full heading path. Nothing in this design checks whether a Source: line is accurate or current, because lanes allocate accountability and do not establish truth.

Skip this if you maintain docs alone with no external commitments, if your reviewers will not read a failing check, or if most of your pages are generated artifacts. In those cases a plain style guide plus honest commit messages will cost less than a lane map you stop maintaining after a month.

If you try the three-lane split, the most useful thing to share back is your lane decision table for that awkward middle case: configuration examples that also imply support.