DEV Community

Avery Lin
Avery Lin

Posted on

Require a Section Ownership Map Before Models Draft API Docs

Generated API documentation fails at merge when models write promises that no schema, fixture, or signed policy can support. A practical fix is to classify every section before drafting begins, then allow models to restate only example and field material. Humans keep versioning, authentication, billing, and support commitments, and a small validator enforces that split in continuous integration. This article proposes that gate as a reproducible workflow, not as a claim about any live production incident.

The method below does not replace OpenAPI review, fixture capture, or legal sign-off for customer-facing language. It only stops a draft from mixing restatable facts with commitments that a model cannot own. Teams that already freeze identifiers or block calendar sentences still miss this failure mode when an Examples heading quietly absorbs a support promise. Treat the ownership map as the first merge input, then treat generated prose as a downstream artifact.

Why heading class is the missing control

Most generation pipelines ask a model for a complete reference page after a schema dump lands in the prompt. That request collapses three different jobs into one blob: restating fields, inventing worked examples, and narrating what the vendor will do next quarter. Reviewers then argue about tone instead of arguing about whether a paragraph was allowed to exist. Classification first reduces that argument to a table that CI can fail in a few seconds.

A heading such as Request Examples can be compiled from recorded cassettes and OpenAPI example fields without inventing status codes. A heading such as Breaking Changes cannot, because deprecation dates and migration windows are policy, not syntax. Mixed headings, including Error Handling, need a split: catalog rows may be drafted from fixtures, while retry and timeout advice stays human-owned. The rest of this workflow encodes that split in YAML, Markdown markers, and a Python check.

The three ownership classes

Use exactly three labels so reviewers do not invent local dialects during a rush. MODEL-DRAFT covers text that restates a named source file and contains no forward-looking vendor commitment. HUMAN-OWNED covers text that binds the organization to behavior, money, calendar, identity, or support. MIXED covers a heading that may contain a drafted table only when a human subsection remains in the same page.

Assign labels to headings, not to whole files, because a single Markdown path often mixes a field table with a support narrative. Record the allowed source glob beside each MODEL-DRAFT heading so a later draft cannot cite chat memory. Record a signer identity beside each HUMAN-OWNED heading so silence cannot pass as approval. Leave MIXED headings in the map only when the human subsection name is also listed.

Artifact: docs-ownership.yaml plus a validator

The artifact is a checked-in map and a script that fails when a draft violates the map. Save the map next to the docs tree, not inside a chat transcript, so history remains reviewable. The YAML below is a proposal for a fictional Payments API; replace paths with your repository layout before use. Do not treat the signer names as real people or as evidence of a shipped product.

# docs-ownership.yaml — proposal, not an executed production policy
version: 1
root: docs/api/
prohibited_in_model_draft:
  - "\\bwe will\\b"
  - "\\bSLA\\b"
  - "\\buptime\\b"
  - "\\bguaranteed\\b"
  - "\\bdeprecated on\\b"
  - "\\bsupport will\\b"
  - "\\bbilling\\b"
  - "\\bprice\\b"
  - "\\bSSO\\b"
  - "\\bretain for\\b"
sections:
  - heading: "Authentication"
    class: HUMAN-OWNED
    signer: "docs-oncall@example.com"
    signature_heading: "Authentication sign-off"
  - heading: "Field reference"
    class: MODEL-DRAFT
    sources:
      - "openapi/payments.yaml"
      - "fixtures/fields/*.json"
  - heading: "Request examples"
    class: MODEL-DRAFT
    sources:
      - "cassettes/payments/*.yaml"
      - "openapi/payments.yaml"
  - heading: "Error catalog"
    class: MIXED
    model_subsection: "Status and payload table"
    human_subsection: "Retry and support policy"
    sources:
      - "cassettes/payments/errors/*.yaml"
    signer: "docs-oncall@example.com"
  - heading: "Versioning and deprecation"
    class: HUMAN-OWNED
    signer: "api-governance@example.com"
    signature_heading: "Versioning sign-off"
Enter fullscreen mode Exit fullscreen mode

The validator reads the map, walks Markdown files under root, and applies three checks. First, every mapped heading must appear in the tree so silent deletions cannot drop a human section. Second, MODEL-DRAFT bodies must not match prohibited commitment patterns and must cite at least one listed source path. Third, HUMAN-OWNED and MIXED human subsections must contain a sign-off line that names the mapped signer. The script is a proposal you can run locally after you point it at real files.

#!/usr/bin/env python3
"""Validate section ownership for generated API docs. Proposal / local check."""
from __future__ import annotations

import pathlib
import re
import sys

try:
    import yaml
except ImportError:
    sys.stderr.write("Install pyyaml before running this check.\n")
    sys.exit: 2

HEADING = re.compile(r"^(#{2,4})\s+(.+?)\s*$", re.M)
CITE = re.compile(r"Source:\s*(\S+)")
SIGN = re.compile(r"Signed-off-by:\s*(\S+)")


def load_map(path: pathlib.Path) -> dict:
    data = yaml.safe_load(path.read_text(encoding="utf-8"))
    if not isinstance(data, dict) or "sections" not in data:
        raise SystemExit("docs-ownership.yaml is missing sections")
    return data


def split_sections(text: str) -> list[tuple[str, str]]:
    matches = list(HEADING.finditer(text))
    out: list[tuple[str, str]] = []
    for index, match in enumerate(matches):
        title = match.group(2).strip()
        start = match.end()
        end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
        out.append((title, text[start:end]))
    return out


def main() -> int:
    repo = pathlib.Path(".")
    spec = load_map(repo / "docs-ownership.yaml")
    root = repo / spec["root"]
    files = list(root.rglob("*.md")) if root.exists() else []
    bodies: dict[str, str] = {}
    for path in files:
        for title, body in split_sections(path.read_text(encoding="utf-8")):
            bodies[title] = body
    prohibited = [re.compile(p, re.I) for p in spec.get("prohibited_in_model_draft", [])]
    failures: list[str] = []
    for row in spec["sections"]:
        heading = row["heading"]
        body = bodies.get(heading, "")
        if not body:
            failures.append(f"missing heading: {heading}")
            continue
        klass = row["class"]
        if klass == "MODEL-DRAFT":
            for pattern in prohibited:
                if pattern.search(body):
                    failures.append(f"commitment language in {heading}: {pattern.pattern}")
            cited = set(CITE.findall(body))
            allowed = {pathlib.Path(s).as_posix() for s in row.get("sources", [])}
            if not cited.intersection(allowed):
                failures.append(f"{heading} cites no mapped source")
        if klass in {"HUMAN-OWNED", "MIXED"}:
            target = body
            if klass == "MIXED":
                sub = row.get("human_subsection", "")
                target = bodies.get(sub, "")
            signer = SIGN.search(target or "")
            if not signer or signer.group(1) != row.get("signer"):
                failures.append(f"unsigned human section: {heading}")
    for line in failures:
        print(line)
    return 1 if failures else 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

A corresponding unit check can lock the classifier without publishing private API text. Keep fixtures tiny and synthetic so the test remains copyable. The snippet below is labeled as an unexecuted example; run it only after you save both files and install PyYAML.

# tests/test_docs_ownership_proposal.py — unexecuted example
from pathlib import Path


def test_model_section_rejects_sla_sentence(tmp_path: Path, monkeypatch):
    (tmp_path / "docs-ownership.yaml").write_text(
        """
version: 1
root: docs/api/
prohibited_in_model_draft:
  - "\\bSLA\\b"
sections:
  - heading: "Request examples"
    class: MODEL-DRAFT
    sources: ["cassettes/payments/create.yaml"]
""".strip(),
        encoding="utf-8",
    )
    page = tmp_path / "docs" / "api" / "create.md"
    page.parent.mkdir(parents=True)
    page.write_text(
        "## Request examples\n\nWe offer a 99.9% SLA.\n\nSource: cassettes/payments/create.yaml\n",
        encoding="utf-8",
    )
    monkeypatch.chdir(tmp_path)
    import validate_docs_ownership as v

    assert v.main() == 1
Enter fullscreen mode Exit fullscreen mode

Numbered workflow

  1. Inventory headings in the current reference tree with a one-line search, then paste unmatched titles into the YAML map as explicit work. rg -n '^#{2,4} ' docs/api is enough to start; do not let a model invent extra headings during this pass. Every new heading is a classification event, not a styling event, and unclassified headings should fail the same check after you add a default deny rule.

  2. Bind each MODEL-DRAFT heading to files that already exist in git, such as OpenAPI documents and recorded HTTP cassettes. If a source file is missing, stop generation rather than asking a model to recall a payload. Cassettes should include status, headers you intend to document, and a body that matches the public schema. Unsigned screenshots and chat logs are not sources under this map.

  3. Draft only mapped MODEL-DRAFT sections, and keep the prompt limited to restating those files. A free-model workspace is useful here when you want a disposable draft without mixing it into the human-owned files. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which can host that isolated draft job, but this workflow does not depend on any named model, quota, or hardware claim.

  4. Write HUMAN-OWNED sections in a separate change, with the sign-off line present before review starts. Do not ask a model to pre-fill those sections for speed, because reviewers then debate wording instead of policy. If a MIXED heading needs a status table, generate only the table rows from cassettes, then leave retry advice blank for the signer. Empty human subsections should fail CI rather than ship as TODO comments.

  5. Run the validator on the review branch and reject the merge when any check fails. A useful command sequence is python3 validate_docs_ownership.py && git diff --check docs/api. Read the failure list as a classification bug, not as a writing-quality bug. After green checks, reviewers still read human-owned prose for accuracy, because the script does not understand product law.

# proposed local sequence — unexecuted in this article
python3 -m pip install pyyaml
python3 validate_docs_ownership.py
rg -n "we will|SLA|deprecated on" docs/api || true
Enter fullscreen mode Exit fullscreen mode

Decision table for common headings

Heading pattern Class Model may draft Human must own Typical source
Field reference MODEL-DRAFT Names, types, required flags None beyond review OpenAPI schema
Request examples MODEL-DRAFT curl and JSON from cassettes Redaction of secrets HTTP cassettes
Error catalog table MIXED Status and payload rows Retry, timeout, support Error cassettes
Authentication HUMAN-OWNED Nothing Token lifetime, SSO, setup Signed policy
Versioning HUMAN-OWNED Nothing Sunset dates, windows Governance record
Billing notes HUMAN-OWNED Nothing Prices, credits, invoices Finance source
Rate limits as numbers MIXED Documented integer fields Burst behavior promises Config plus signer

Read the table as a default, then tighten rows when your product has extra legal exposure. Numeric rate limits that already exist as configuration may look like schema, yet promising burst behavior is still a commitment. When a heading could sit in two rows, choose the more restrictive class and move extractable tables into a child heading. Do not add a fourth class to express discomfort; recut the page instead.

What a model may draft, in concrete terms

A model may restate property names, types, enumerations, and required flags that already appear in the OpenAPI document. It may format cassette requests as fenced curl blocks when every header and field is present in the cassette. It may list error codes that appear in recorded responses, together with the payload keys those responses actually contain. It may cross-link to schema anchors that exist in the same branch.

A model may not invent an example status code because the happy-path cassette was empty. It may not translate a 429 body into a promise that retries will succeed after a stated delay. It may not fill Authentication with a recommended identity vendor because the schema only shows a bearer header. It may not turn a field description into a compatibility calendar, even when the description mentions a future rename.

Keep drafted sections short enough that a reviewer can compare them to the source file in one screen. Long narrative bridges between examples are usually human-owned, because they imply recommended production usage. If the draft needs a sentence that cannot cite a mapped path, move that sentence into a HUMAN-OWNED heading before regeneration. Regeneration should replace only MODEL-DRAFT bodies so signatures do not vanish.

What a human must own

Humans own any sentence that would still matter if the schema file were deleted tomorrow. That set includes authentication product behavior, session lifetime, and whether leaked tokens are revoked automatically. It includes deprecation dates, partner migration windows, and statements that old clients will keep working. It includes pricing, credit grants, data-retention intervals, and support hours.

Humans also own negative space: features the schema does not expose yet, and regional availability that no fixture records. If a reviewer wants a Getting Started path that includes console clicks, that path is human-owned unless each click is backed by a screenshot protocol you actually maintain. The sign-off line is not ceremony; it names who is accountable when the sentence is wrong. Rotate signers in the YAML when on-call rotations change, then fail old signatures on the next run.

Limitations and who should not use this

This workflow assumes you already store OpenAPI documents and HTTP cassettes in git, or can start doing so without blocking a release. It does not measure draft quality, latency, or cost, and it does not claim that free model access produces correct examples. The prohibited-language list is a coarse net; committed teams can still smuggle promises through novel wording. The validator does not execute APIs, so a cassette that itself contains fiction will pass if the map cites it.

Do not use this approach for marketing pages, investor letters, incident reports, or legal terms, where ownership is entirely human and generation adds review load. Do not use it when the public API is still unstable and headings change faster than the map can be updated. Do not use it as a substitute for secret scanning; example redaction remains a separate control. Skip the free-server drafting step when your fixtures include production-like personal data that cannot leave your existing boundary.

If your docs set is a single README with no headings, the map will only create ceremony. If reviewers will not refuse a merge on validator failure, the YAML becomes fiction and should not be added. In those cases, write the human-owned pages first and delay generation until the heading inventory is stable. The useful outcome is a boring green check plus a short signed policy, not a longer model essay.

The ownership map is the control you can keep when prompts, models, and servers change. Compile examples from files, sign promises as people, and fail the merge when those jobs share a heading. If you already draft in a workspace that provides free model access and a free server option, attach this map before the first generation call rather than after review fatigue. That is the entire method.

Top comments (0)