DEV Community

Avery Lin
Avery Lin

Posted on

Section-Level Ownership: A Draft/Own Contract for Generated Docs

Contract testing for documentation works best when the review unit is a section, not an entire page. A machine-readable ownership matrix tells the model which blocks it may draft, tells the reviewer which blocks demand a signature, and lets CI reject any merge that blurs the boundary. This article walks through the workflow, ships a small gate script, and closes with the teams that should not adopt it.

The failure is blurred ownership, not hallucination

Most generated-doc debates center on fabrication, yet hallucination is only the visible symptom. The quieter failure is that ownership dissolves: a model drafts an entire page, a reviewer approves the whole page, and no one can say which paragraph encodes product truth and which paragraph merely restates code. Once such a page merges, the document has one author and zero owners.

A workable remedy is to split documents into zones before generation starts. The split is not about style preference but about falsifiability: a claim belongs to the model when an executable check can contradict it, and to a human when only domain knowledge or production behavior can contradict it. Parameter tables and usage snippets are falsifiable by running the code, while invariants and compatibility promises are not.

Zone model: M, H, and X

The workflow uses three zones with distinct review obligations.

Zone M (model-draftable) covers installation steps, parameter tables, usage examples, and API reference entries. Every M section must be executable or diffable, which is why a model may draft it without a named human author. The gate verifies provenance only, not correctness, because correctness is delegated to the drift step.

Zone H (human-owned) covers invariants, failure modes, compatibility notes, and security caveats. These claims describe the world beyond the repository, so they require a named reviewer and a review timestamp before the gate passes. A missing signature is a merge blocker; a stale signature is a drift warning.

Zone X (hybrid) covers conceptual overviews and design rationale. The model produces a first draft, a human rewrites it, and the gate only checks that a rewrite marker exists in frontmatter. This zone exists because prose quality is exactly where humans add leverage over models.

Step 1: encode the boundary as a config file

A boundary that lives only in a design doc will evaporate during the next refactor. Put the matrix in a small YAML file that maps heading markers to zones:

# docs-ownership.yml
default: X
zones:
  - marker: "## Usage"
    owner: M
  - marker: "## Examples"
    owner: M
  - marker: "## API Reference"
    owner: M
  - marker: "## Invariants"
    owner: H
  - marker: "## Failure Modes"
    owner: H
  - marker: "## Compatibility"
    owner: H
  - marker: "## Overview"
    owner: X
Enter fullscreen mode Exit fullscreen mode

The mapping is deliberately small. If a heading is absent from the file, the default zone X applies, and if a new heading appears with no owner, the gate should flag it for a decision.

Step 2: annotate docs with review metadata

Every document that contains an M or H section needs provenance or review metadata in its frontmatter. The gate treats the pair of review keys as a signature:

---
title: "Rate Limiter"
model_generated: true
reviewed_by: avery
reviewed_at: 2026-08-28
---
Enter fullscreen mode Exit fullscreen mode

A document with only M sections keeps the provenance flag and drops the review keys; a document with an H section needs both review keys. The timestamp is not decorative: a doc reviewed before its last code change fails the gate, because git history can prove that the signature predates the edit that touched the H section.

Step 3: run the gate locally

The gate is a single Python script that reads the config, scans every Markdown file, and exits non-zero on a violation. The version below is a working starting point:

#!/usr/bin/env python3
"""Enforce the draft/own boundary for generated documentation."""
import re
import sys
from pathlib import Path

import yaml

REVIEW_KEYS = {"reviewed_by", "reviewed_at"}
SECTION_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)


def load_config(path: Path) -> dict:
    with path.open() as fh:
        return yaml.safe_load(fh)


def check_doc(doc: Path, config: dict) -> list[str]:
    text = doc.read_text()
    frontmatter = {}
    if text.startswith("---"):
        frontmatter = yaml.safe_load(text.split("---", 2)[1]) or {}
    present = {m.group(1).strip().lower(): m.start()
               for m in SECTION_RE.finditer(text)}
    errors = []
    for zone in config["zones"]:
        marker = zone["marker"].lstrip("#").strip().lower()
        if marker not in present:
            continue
        if zone["owner"] == "H" and not REVIEW_KEYS.issubset(frontmatter):
            errors.append(f"{doc}: {zone['marker']} needs a human signature")
        if zone["owner"] == "M" and "model_generated" not in frontmatter:
            errors.append(f"{doc}: {zone['marker']} needs model_generated: true")
    return errors


def main() -> int:
    config = load_config(Path("docs-ownership.yml"))
    docs = sorted(Path("docs").glob("*.md"))
    failures = []
    for doc in docs:
        failures.extend(check_doc(doc, config))
    if failures:
        print("\n".join(failures))
        return 1
    print(f"gate passed for {len(docs)} docs")
    return 0


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

The M branch matters as much as the H branch. If a generated section loses its provenance marker, the next human edit silently converts a model claim into an authored claim, and the ownership boundary fades without any visible error.

Step 4: wire the gate into CI

Local gates are advisory; merge gates are enforceable. Add the script to a Makefile target and call it from your existing pipeline:

.PHONY: doc-gate
doc-gate:
    python3 doc_gate.py
Enter fullscreen mode Exit fullscreen mode

The gate is a single file with one dependency, so runner choice is not a constraint. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free model access can draft the M sections before the diff step, and the free server option can host the gate without a paid runner.

Step 5: regenerate and diff to catch drift

A signature only proves that a person once agreed with the text; it says nothing about whether the text still matches the code. Add a drift check that regenerates every M section with the model and diffs the result against the committed section, assuming a small wrapper like docs/regenerate.py that calls the model for one section:

# regenerate the Usage section and compare with the committed version
docs/regenerate.py --section "## Usage" > /tmp/usage.md
git diff --no-index --exit-code docs/usage.md /tmp/usage.md
Enter fullscreen mode Exit fullscreen mode

A non-empty diff means the committed text no longer reflects the current implementation, and that signal should reach the human who signed the page. Drift checks are the difference between ownership as a one-time event and ownership as a maintenance loop.

Decision table: who drafts what

Section type Zone Falsified by Review burden
Usage, examples, parameters M Executing the example Model provenance flag
API reference M Type checks and return values Model provenance flag
Overview, rationale X Human reading Human rewrite marker
Invariants, failure modes H Production behavior Named reviewer and timestamp
Compatibility, security H External reality Named reviewer and timestamp

The table is the whole contract in one view: the model may draft only what a machine can contradict, and a human must own everything that a machine cannot.

Who should not use this approach

Startups shipping a single generated README should skip this workflow, because the matrix overhead exceeds the page itself. Teams with unstable heading names will fight the marker matching more than the models. The approach also fails when reviewers sign H sections without reading them, since no gate can detect a rubber stamp. In those cases, invest in the review culture first and add the contract later.

The merge is the verdict

A generated document is not wrong because a model wrote it; it is wrong when the boundary between drafted claims and owned claims disappears. Encode the zones, sign the human parts, and let CI hold the line, and the next model draft becomes a manageable contribution instead of an ownership vacuum. The natural next step is to run this gate on one real page and watch which sections survive the diff.

Top comments (0)