DEV Community

Avery Lin
Avery Lin

Posted on

Assign Draft Rights Per Heading Before a Model Touches API Docs

Generated API docs fail most often at the heading level, not at the token level. Models fill every empty section because prompts treat an outline as a request to complete. Human promises then hide inside overviews, authentication notes, and example narratives that no schema can support. The practical fix is to assign draft rights per heading before any model run starts.

Why heading rights beat after-the-fact filters

Post-generation linters still allow the model to invent a support policy, then ask a reviewer to delete it. That pattern wastes review time and trains teams to skim generated prose instead of checking evidence. A heading that the model should never write should never appear in the prompt, the context pack, or the output schema. Reviewers then inspect only tables and restatements that a schema, fixture, or OpenAPI path can support.

Section ownership also keeps tutorial pages and reference pages from collapsing into one generated blob. Reference tables can be compiled from machine-readable sources with little narrative risk. Tutorial headings almost always encode product intent, audience assumptions, and operational promises that remain human work. Mixing those classes in a single completion is the usual source of silent policy drift.

Ownership classes you can encode

Treat every heading as one of four classes before a draft job is scheduled. Record the class in version control beside the doc tree, not inside a chat transcript. The classes below are a proposal for public HTTP APIs; other products will need extra rows.

Class Model may draft Human must own Allowed sources
schema_table yes no OpenAPI, JSON Schema, protobuf
recorded_example yes no checked-in fixtures, reviewed HAR
identifier_index yes no frozen path and field lists
promise_prose no yes none; leave a stub

promise_prose covers purpose statements, authentication policy, rate limits, SLAs, deprecation calendars, support contacts, and any "you will typically" sentence. recorded_example covers request and response bodies only when a named fixture file is cited on the same heading. If a heading has no source column, classify it as promise_prose by default, and refuse to treat the gap as draftable work.

Workflow

Follow these numbered steps on a branch that already has an OpenAPI file and a docs outline. Label the commands as a proposed local workflow; they are not reported production metrics. Keep the manifest in the same pull request as heading edits so classification cannot drift from the rendered tree.

1. Inventory headings from the outline

Export the heading tree before any model sees the repository. A small script is enough when the outline already lives in Markdown, and the output should be discarded after the YAML manifest exists.

# proposal: extract ATX headings from a stub outline
python3 - <<'PY'
from pathlib import Path
import re
text = Path("docs/api/outline.md").read_text(encoding="utf-8")
for i, line in enumerate(text.splitlines(), 1):
    m = re.match(r"^(#{2,4})\s+(.*)$", line)
    if m:
        print(f"{len(m.group(1))}|{m.group(2).strip()}|{i}")
PY
Enter fullscreen mode Exit fullscreen mode

Keep the document title under human control, and inventory H2 through H4 as the units you will grant or deny. Store the printed rows only until ownership.yaml exists, then delete the ad-hoc list so the YAML file remains the only authority. Outline drift after that point should fail closed in continuous integration rather than being repaired by another model pass.

2. Write a section ownership manifest

Create docs/api/ownership.yaml with stable heading identifiers, not generated prose. Identifiers should match the outline after a light slug rule so CI can join rows to rendered headings without fuzzy matching.

# proposal: docs/api/ownership.yaml
version: 1
slug_rule: "lower-hyphen"
sections:
  - id: overview
    heading: "Overview"
    class: promise_prose
  - id: authentication
    heading: "Authentication"
    class: promise_prose
  - id: endpoints
    heading: "Endpoints"
    class: schema_table
    sources: ["openapi/openapi.yaml"]
  - id: error-envelope
    heading: "Error envelope"
    class: schema_table
    sources: ["openapi/components/schemas/Error.yaml"]
  - id: create-order-example
    heading: "Create order example"
    class: recorded_example
    sources: ["fixtures/http/create_order_201.json"]
  - id: rate-limits
    heading: "Rate limits"
    class: promise_prose
  - id: deprecation
    heading: "Deprecation"
    class: promise_prose
  - id: support
    heading: "Support"
    class: promise_prose
Enter fullscreen mode Exit fullscreen mode

Refuse to start a draft job when any heading in the outline lacks a row, because missing classification is not an invitation to generate. Add a row, or delete the heading from the public outline, before the next step runs. A heading without sources is still not draftable; it is an incomplete human section that must stay a stub.

3. Carve the model prompt down to draftable sections

Build the prompt from the manifest rather than from the full Markdown outline. Include only schema_table, recorded_example, and identifier_index rows, plus the files listed in sources. Omit promise_prose headings entirely so the model cannot complete them, even with hedging language that looks review-friendly.

# proposal: tools/carve_doc_prompt.py
from pathlib import Path
import yaml

manifest = yaml.safe_load(Path("docs/api/ownership.yaml").read_text())
DRAFTABLE = {"schema_table", "recorded_example", "identifier_index"}

parts = ["Restate only the listed sources. Do not add headings.", ""]
for section in manifest["sections"]:
    if section["class"] not in DRAFTABLE:
        continue
    parts.append(f"## {section['heading']}")
    parts.append(f"class: {section['class']}")
    for src in section.get("sources", []):
        body = Path(src).read_text(encoding="utf-8")
        parts.append(f"SOURCE {src}")
        parts.append(body)
        parts.append("")
Path("tmp/draftable-prompt.md").write_text("\n".join(parts), encoding="utf-8")
Enter fullscreen mode Exit fullscreen mode

The carved file is the only model input for the draft job. Do not paste README marketing, issue comments, or prior chat answers into that file, because those texts are promise-shaped even when they look like documentation. Truncated schemas are also unsafe inputs; pass complete source files for every draftable heading.

A later step in this workflow can send only draftable headings to a free model endpoint. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that narrow draft job and the ownership check without granting extra section rights.

4. Reassemble stubs so human headings stay empty

After the model returns Markdown, join it with the original outline by heading identifier rather than by paragraph similarity. Copy model output only under draftable headings, and write a fixed stub under every promise_prose heading so reviewers see the gap instead of fluent filler. Similarity matching is a merge bug here, because a cautious quota paragraph can still look like a neighboring field table.

<!-- HUMAN_OWNED:rate-limits -->
_This section is empty on purpose. A human must write rate-limit numbers,
burst behavior, and enforcement language after an operations review._
Enter fullscreen mode Exit fullscreen mode

Never let the assembler copy a model paragraph into a stub, even when the paragraph looks cautious, because cautious language about quotas is still a promise. The stub comment is the merge contract: generated files may replace schema_table bodies and must preserve HUMAN_OWNED markers byte-for-byte. Regeneration jobs should overwrite only draftable bodies and must leave stub files untouched.

5. Fail CI when human-owned headings gain prose

The checker below is a proposed gate, not a measured production suite. It flags three failures: unknown headings, draftable headings without a source citation, and human-owned headings whose body lost the marker. Run it beside your OpenAPI validator so a docs change cannot merge with only a green HTML build.

# proposal: tools/check_doc_ownership.py
import re, sys, yaml
from pathlib import Path

ROOT = Path("docs/api")
manifest = yaml.safe_load((ROOT / "ownership.yaml").read_text())
text = (ROOT / "reference.md").read_text(encoding="utf-8")
parts = re.split(r"\n(?=## )", text)
bodies = {}
for part in parts:
    first, _, rest = part.partition("\n")
    title = first.replace("## ", "", 1).strip()
    bodies[title] = rest

errors = []
known = {s["heading"] for s in manifest["sections"]}
for heading in bodies:
    if heading and heading not in known:
        errors.append(f"unknown heading: {heading}")

DRAFTABLE = {"schema_table", "recorded_example", "identifier_index"}
for section in manifest["sections"]:
    body = bodies.get(section["heading"], "")
    if section["class"] == "promise_prose":
        marker = f"HUMAN_OWNED:{section['id']}"
        if marker not in body:
            errors.append(f"missing stub marker: {section['id']}")
        prose = body.replace(marker, "")
        if re.search(r"\b(will|must|typically|guarantee|SLA)\b", prose, re.I):
            errors.append(f"promise language under {section['id']}")
    elif section["class"] in DRAFTABLE:
        for src in section.get("sources", []):
            if src not in body:
                errors.append(f"missing source citation {src} in {section['id']}")

if errors:
    print("\n".join(errors))
    sys.exit(1)
print("ownership check passed")
Enter fullscreen mode Exit fullscreen mode
python3 tools/check_doc_ownership.py
Enter fullscreen mode Exit fullscreen mode

A pretty site that invented a rate limit is still a broken contract. Treat checker failures as merge blockers equal to a broken schema, not as documentation nits for a later cleanup ticket.

What the model may draft, restated

Under this workflow the model may restate field tables, path lists, and fixture-backed examples. It may rename columns only when the manifest says the source already contains those names. It may not write overviews, security rationale, migration dates, or client SDK tutorials that imply an official library. It also may not invent status codes, default values, or enum members that are absent from the cited source file.

Humans own every sentence that would still be true if the OpenAPI file were deleted. That test is coarse and useful during review, because it separates restatement from promise without a semantics model. If the paragraph survives deletion of machine sources, it is a promise, and the heading should have been promise_prose before the draft job ran.

Limitations

The checker does not understand meaning; it understands markers, citations, and a small English word list. A model can still smuggle a promise into a schema_table description if the OpenAPI file already contains that promise. Garbage sources still produce garbage tables, so source review remains a human gate that this workflow does not replace.

Slug mismatches will fail closed, which is correct and annoying for writers who retitle sections late. Heading edits require a manifest edit in the same pull request, and multilingual sites must not auto-translate promise_prose stubs. Free model drafts will still hallucinate enum values when the source file is truncated, so the carve script must pass complete schemas rather than summaries.

The free server option is only useful here as a place to run carve, complete, and check as one job. It does not replace the ownership file, and it does not make a heading draftable. Teams that skip the manifest and only host the model are not following this workflow.

Who should not use this approach

Do not use heading rights if your public docs are primarily narrative essays with no schema. The manifest would mark almost every section human-owned, and the model would have nothing honest to draft. Do not use it to launder marketing pages through a generator so they look like reference material, because the output would still be promise_prose with extra steps.

Skip the workflow when a regulator requires all-human authorship with no model in the pipeline at all. The right move then is to keep models off the docs branch, not to classify headings and hope a stub survives review. Also skip it for internal wikis where incomplete stubs would confuse readers more than a slightly wrong paragraph would, because empty sections are a user-facing cost.

Review order that keeps classes honest

Reviewers should read ownership.yaml before reference.md, and they should stop when a heading class looks wrong. Class errors are cheaper to fix than mixed prose that already sounds like the rest of the site. After classes look right, review source citations, then read only the draftable bodies, leaving stubs for a later human commit.

Human stubs should be filled in a separate commit from generated tables. That split keeps blame useful and prevents a regenerate step from clobbering a signed rate-limit paragraph. When a regenerate is needed, rerun carve and check, then leave promise_prose files untouched so signed sentences cannot be rewritten by a restater.

If you already draft schema tables from a free model endpoint, keep the ownership check on the same worker that writes reference.md. The check is the product of this workflow; the model is only a restater of files you already trust.

Top comments (0)