DEV Community

Avery Lin
Avery Lin

Posted on

Five Lines a Human Must Write Before a Model Drafts the Page

A page has an owner only when a named human has committed five specific sentences about it, and everything else in the file is fair game for a model draft. The gate below enforces that split on every pull request, using a flat manifest, a human allow-list, and roughly eighty lines of standard-library Python. It is cheap to adopt, and it fails loudly in the one case that matters.

The failure this prevents

Reviewers read prose, not provenance, and a fluent paragraph does not carry a visible author. A confident description of retry semantics looks identical whether a backend engineer verified it against the worker or a model assembled it from three similar pages. Page-level review rules do not catch this, because each paragraph is short enough to skim and plausible enough to approve. The result is documentation that nobody can defend when a support ticket asks a hard question.

Section-level conventions have the same weakness, since a rule like "humans review the limits section" decays the moment a model rewrite moves the limits sentence one heading up. The anchors below are structural rather than editorial: they live in a checked-in manifest, they reference commits, and they can be validated without reading the prose at all.

The five anchors

Each anchor answers a question that the model is not in a position to settle, no matter how much repository context it receives.

Anchor Question it answers Why a model cannot own it
audience Who is this page written for, and what do they already know? Requires knowing who actually files tickets
job What single task should a reader finish after reading? Requires a commitment about supported use
decision What must a reader decide, and what is the default? Requires a policy the team will stand behind
non_goal What does this page deliberately not cover? Requires the authority to decline scope
failure_mode What breaks if the reader applies this wrongly? Requires operational or incident knowledge

These five are claims about scope and consequence rather than facts about the codebase. A model can restate what exists in a repository; it cannot promise what a team supports or refuse what it will not. The anchors are therefore the smallest set of sentences that turn a page into someone's responsibility.

Step 1 — Mark the regions a model may draft

Use an HTML comment so the marker is visible in source and invisible when the page renders. Keep the marker narrow, and let the anchors sit outside it.

# Queue retries

## Who this is for <!-- anchor:audience -->

## What you will do <!-- anchor:job -->

<!-- draft:model -->
The worker retries a failed job up to five times, then moves it to the dead-letter
queue. Each retry waits `2^n` seconds with full jitter applied before the delay.
<!-- /draft:model -->

## What this page does not cover <!-- anchor:non_goal -->
Enter fullscreen mode Exit fullscreen mode

Only the <!-- draft:model --> token is load-bearing for the checker, and the anchor comments exist for human navigation and diff review. If you already use a different drafting marker, change one regular expression in the script.

Step 2 — Record the anchors in a flat manifest

Keep the manifest as JSON so the checker needs no third-party parser and can run in a minimal container.

{
  "docs/queue-retries.md": {
    "audience": {
      "text": "Backend engineers who own a worker in production",
      "by": "avery",
      "commit": "9c1f4ab",
      "verified_at": "2026-10-12"
    },
    "job": {
      "text": "Configure retry counts without losing failed jobs",
      "by": "sam",
      "commit": "4d7e021",
      "verified_at": "2026-10-09"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Three more entries follow the same shape, and commit points at the change that introduced or last re-verified the anchor text. That pointer is what makes the claim auditable later, when the page has been rewritten twice and nobody remembers who promised what.

Step 3 — Give drafting its own git identity

Configure the drafting tool to commit under a dedicated address that never appears in the human allow-list. This single convention is what prevents a model-authored commit from being used as evidence of human ownership.

# one-time, in the drafting environment
git config user.name "draft-bot"
git config user.email "draft-bot@example.invalid"
Enter fullscreen mode Exit fullscreen mode
{
  "avery": "avery@example.com",
  "sam": "sam@example.com"
}
Enter fullscreen mode Exit fullscreen mode

The allow-list lives in docs/people.json and should match real addresses used for signed commits, not display names. When someone leaves the team, remove them from the file and the gate will start naming every anchor they previously owned.

Step 4 — Run the gate in pre-commit and CI

The script below resolves each anchor commit, compares the author against the allow-list, confirms the commit actually touched the manifest, and checks anchor age.

#!/usr/bin/env python3
'''anchor_gate.py - fail when a model-drafted page has no human-owned anchors.'''
from __future__ import annotations

import datetime as dt
import json
import re
import subprocess
import sys
from pathlib import Path

REQUIRED = ('audience', 'job', 'decision', 'non_goal', 'failure_mode')
DRAFT_MARK = re.compile(r'<!--\s*draft:model\s*-->')
MAX_AGE_DAYS = 90
ROOT = Path(__file__).resolve().parent


def git(*args: str) -> str:
    done = subprocess.run(['git', *args], cwd=ROOT, capture_output=True, text=True)
    return done.stdout.strip()


def load(name: str) -> dict:
    return json.loads((ROOT / name).read_text(encoding='utf-8'))


def drafted_pages() -> list[str]:
    pages = []
    for page in sorted(ROOT.glob('docs/**/*.md')):
        if DRAFT_MARK.search(page.read_text(encoding='utf-8')):
            pages.append(str(page.relative_to(ROOT)))
    return pages


def check_page(page: str, manifest: dict, humans: dict) -> list[str]:
    entry = manifest.get(page)
    if entry is None:
        return [f'FAIL {page}: drafted but missing from docs/anchors.json']
    problems = []
    for key in REQUIRED:
        anchor = entry.get(key) or {}
        text = (anchor.get('text') or '').strip()
        by = anchor.get('by') or ''
        sha = anchor.get('commit') or ''
        if not (text and by and sha):
            problems.append(f'FAIL {page} {key}: anchor incomplete')
            continue
        author = git('show', '-s', '--format=%ae', sha).lower()
        if humans.get(by, '').lower() != author:
            problems.append(
                f'FAIL {page} {key}: commit {sha[:7]} authored by '
                f'{author or "unknown"}, not listed human {by}'
            )
        elif 'docs/anchors.json' not in git('show', '--name-only', '--format=', sha):
            problems.append(f'FAIL {page} {key}: commit {sha[:7]} never touched the manifest')
        try:
            age = (dt.date.today() - dt.date.fromisoformat(anchor.get('verified_at', ''))).days
        except ValueError:
            problems.append(f'FAIL {page} {key}: verified_at missing or not ISO')
            continue
        if age > MAX_AGE_DAYS:
            problems.append(f'FAIL {page} {key}: anchor {age} days old, owner must re-verify')
    return problems


def main() -> int:
    manifest, humans = load('docs/anchors.json'), load('docs/people.json')
    pages = drafted_pages()
    problems = [p for page in pages for p in check_page(page, manifest, humans)]
    if problems:
        print('\n'.join(problems))
        return 1
    print(f'OK {len(pages)} drafted page(s) carry human anchors')
    return 0


if __name__ == '__main__':
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Wire it into pre-commit first so authors see the message before a review starts. The CI job is the same command, and it should run on pull requests that touch docs/ rather than on every push.

# .pre-commit-config.yaml (excerpt)
repos:
  - repo: local
    hooks:
      - id: anchor-gate
        name: doc anchor gate
        entry: python3 anchor_gate.py
        language: system
        files: ^docs/.*\.md$
Enter fullscreen mode Exit fullscreen mode

Step 5 — Route the failures

The message determines the fix, and the three cases are worth separating in your team's own runbook.

Failure Who acts Action
missing from docs/anchors.json Page author Write the five anchors before requesting review
authored by ..., not listed human Reviewer Replace the evidence with a human commit, do not edit the allow-list
anchor N days old Named owner Re-verify the claim against the current code, then bump verified_at
never touched the manifest Author Anchor commit must include the manifest change in the same commit

The second row is the one people are tempted to shortcut by adding the bot address to people.json. Doing so converts a real ownership check into a formality, and the gate will happily pass pages that no human has read.

Test plan you can run in ten minutes

Build three fixtures from a copy of your docs directory and confirm the exit codes before trusting the gate in CI.

mkdir -p test/fixtures && cp -r docs test/fixtures/missing
python3 - <<'PY'
import json, pathlib
p = pathlib.Path('test/fixtures/missing/docs/anchors.json')
p.write_text(json.dumps({}), encoding='utf-8')
PY
(cd test/fixtures/missing && python3 anchor_gate.py; echo "exit=$?")
Enter fullscreen mode Exit fullscreen mode

Expected output: FAIL docs/queue-retries.md: drafted but missing from docs/anchors.json with exit=1. For the stale case, set one verified_at to 2025-01-01 and expect a message naming that anchor and its age in days. For the authorship case, point one commit at any commit made by the drafting identity and expect the not a listed human message; if that commit still passes, your allow-list is wrong.

What the model may draft, and what stays human

The split is easier to hold when it is written down once, because models are genuinely good at the left column and unreliable at the right one.

Draftable by a model Human-owned, with dated evidence
Overview prose and section summaries Limits, preconditions, and supported configurations
Worked examples built from code in the repo Security, compliance, and data-handling statements
Glossary restatement and term expansion Any number a reader might budget against
Error-message transcription and formatting Deprecation dates and migration order
Reordering an existing page without changing claims Claims about what the team will not support

The right column is not a comment on model quality; it is a comment on who can be asked to defend the sentence later. A model can draft a limits section that reads well, and a human still has to own whether the limit is true this quarter.

Running the loop on one machine

The drafting half needs a model endpoint, and the gate needs somewhere to run on a schedule if you want the staleness check to fire without a pull request. MonkeyCode offers free model access and a free server option, which is enough to draft marked regions and to run anchor_gate.py as a nightly job against the manifest; the availability claims are the operator's, so verify current terms yourself before you depend on them.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Start with one page that has caused a support ticket, add the five anchors, and let the gate run in report-only mode for a week before it blocks anything. If that page becomes easier to defend after a model rewrite, the same loop will hold for the next twenty.

Limitations and who should not use this

The gate proves that a named human committed an anchor, not that the anchor is correct, so a confident wrong claim still passes. Re-verification is a manual 90-day chore, and a busy team will let dates slip unless the nightly job has an owner of its own.

It also adds real friction: expect roughly twenty minutes per page on the first pass, and more if the manifest has to be reconstructed after a docs migration. Pages generated from source, such as API references and schema docs, get no benefit because their claims are already traceable to a definition.

Skip this approach for incident timelines where speed dominates, for throwaway notes, and for single-maintainer projects where the owner is never in question. Ownership is a small artifact you can check, and the point of the gate is simply to stop that artifact from quietly disappearing in the next model-assisted rewrite.

Top comments (0)