DEV Community

Avery Lin
Avery Lin

Posted on

Give Every API Heading a Draft Policy Before a Model Fills It

Generated API documentation stays honest when every heading is a typed slot with an explicit draft policy. Models may restate request shapes, status tables, and recorded examples without inventing product policy. Humans must still own compatibility promises, security claims, and support-window language that no schema can prove. The workflow below treats headings as typed slots, then rejects any model draft that crosses into a commitment slot.

Blank-prompt doc generators collapse three different jobs into one fluent paragraph and hide the seams. Shape restatement is recoverable from OpenAPI, JSON Schema, and checked-in HTTP transcripts. Commitment language is not recoverable, because it encodes what a team will honor after the schema changes. When a model mixes those jobs, reviewers hunt invented guarantees instead of checking structure, examples, and omitted fields.

Four draft policies, one policy per heading

Assign every H2 and H3 in the published template exactly one of four policies. The policy is data, not a reviewer's memory, and it travels with the heading identifier through CI. A model may write into a slot only when the policy permits restatement from extractable inputs. Anything else remains a parked placeholder or a human-only section.

  1. restate — Fill only from extractable fields, enumerated status codes, and recorded transcripts.
  2. hybrid — Allow a walkthrough draft, then require a human to approve every imperative sentence.
  3. park — Keep a visible placeholder until a named owner writes the section.
  4. forbid — Reserve the heading for humans; model output must not include this identifier.

The taxonomy is intentionally small so a validator can enforce it without a scoring model. Teams that want extra nuance should add metadata on the slot, not extra policy names. Extra names usually become undocumented folklore, which is the failure mode this workflow is designed to remove.

Artifact: a slot map the compiler can refuse

The following docslots.json file is a compact example, not an audit of a live production API. It binds heading identifiers to policies, owners, and allowed source kinds. Source kinds constrain what a restatement job may read, which keeps chat history and marketing copy out of the compile inputs.

{
  "template_id": "public-http-api-v1",
  "heading_slots": [
    {
      "id": "auth.headers",
      "heading": "Authentication headers",
      "policy": "restate",
      "sources": ["openapi.securitySchemes", "openapi.parameters"]
    },
    {
      "id": "errors.catalog",
      "heading": "Error catalog",
      "policy": "restate",
      "sources": ["openapi.responses", "fixtures.http"]
    },
    {
      "id": "guides.create-order",
      "heading": "Create an order",
      "policy": "hybrid",
      "sources": ["fixtures.http", "openapi.paths"],
      "approver": "docs-oncall"
    },
    {
      "id": "compat.windows",
      "heading": "Compatibility window",
      "policy": "park",
      "owner": "api-council"
    },
    {
      "id": "security.posture",
      "heading": "Security posture",
      "policy": "forbid",
      "owner": "security-review"
    }
  ],
  "restate_forbidden_lexicon": [
    "guarantee",
    "sla",
    "always available",
    "never break",
    "backward compatible",
    "we will support",
    "encrypted at rest",
    "99.9"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pair the map with a heading skeleton so missing slots are obvious before anyone prompts a model. The skeleton is ordinary Markdown with machine-readable fences around empty bodies. Empty park and forbid bodies are valid; empty restate bodies are compile errors.

## Authentication headers <!-- slot:auth.headers policy:restate -->

## Error catalog <!-- slot:errors.catalog policy:restate -->

## Create an order <!-- slot:guides.create-order policy:hybrid -->

## Compatibility window <!-- slot:compat.windows policy:park -->

> PARKED: api-council must write support dates and deprecation rules.

## Security posture <!-- slot:security.posture policy:forbid -->

> HUMAN-ONLY: security-review owns this section.
Enter fullscreen mode Exit fullscreen mode

A validator that fails closed

The example checker below walks the slot map, the skeleton, and a model output directory. It fails if a restate slot is empty, if model output contains a forbid heading, or if forbidden lexicon appears in a restate body. Treat it as a gate, not as a style linter for human prose.

#!/usr/bin/env python3
"""Example slot validator. Unexecuted against any private API in this article."""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

SLOT_RE = re.compile(r"<!-- slot:(?P<id>[\w.-]+) policy:(?P<policy>\w+) -->")
HEADING_RE = re.compile(r"^#{2,3} (.+)$", re.M)


def load_map(path: Path) -> dict:
    data = json.loads(path.read_text())
    ids = [slot["id"] for slot in data["heading_slots"]]
    if len(ids) != len(set(ids)):
        raise SystemExit("duplicate slot ids in docslots.json")
    return data


def skeleton_slots(text: str) -> dict[str, str]:
    found = {}
    for match in SLOT_RE.finditer(text):
        found[match.group("id")] = match.group("policy")
    return found


def body_for(slot_id: str, markdown: str) -> str:
    parts = re.split(r"(?=^#{2,3} )", markdown, flags=re.M)
    for part in parts:
        if f"slot:{slot_id} " in part:
            return part
    return ""


def main() -> int:
    root = Path(sys.argv[1])
    spec = load_map(root / "docslots.json")
    skeleton = (root / "template.md").read_text()
    model_md = (root / "model-draft.md").read_text() if (root / "model-draft.md").exists() else ""
    mapped = {s["id"]: s for s in spec["heading_slots"]}
    present = skeleton_slots(skeleton)
    errors: list[str] = []

    for slot_id, slot in mapped.items():
        if slot_id not in present:
            errors.append(f"missing skeleton slot {slot_id}")
            continue
        if present[slot_id] != slot["policy"]:
            errors.append(f"policy drift on {slot_id}")
        body = body_for(slot_id, model_md)
        if slot["policy"] == "restate" and len(body.strip()) < 40:
            errors.append(f"empty restate slot {slot_id}")
        if slot["policy"] == "forbid" and slot["heading"].lower() in model_md.lower():
            errors.append(f"model wrote forbid heading {slot_id}")
        if slot["policy"] in {"restate", "hybrid"}:
            lower = body.lower()
            for token in spec["restate_forbidden_lexicon"]:
                if token in lower:
                    errors.append(f"commitment token {token!r} in {slot_id}")

    extra = [sid for sid in present if sid not in mapped]
    errors.extend(f"unmapped heading {sid}" for sid in extra)
    if errors:
        print("\n".join(errors))
        return 1
    print("slot coverage ok")
    return 0


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

Run the checker on a fixture directory before any publishing job is allowed to copy Markdown. The command below assumes the slot map, skeleton, and model draft sit in docs/slots.

python3 tools/check_docslots.py docs/slots
Enter fullscreen mode Exit fullscreen mode

A second command should prove that a planted commitment phrase fails. Keep that negative fixture next to the validator so future edits cannot weaken the lexicon by accident.

printf '%s\n' '## Authentication headers <!-- slot:auth.headers policy:restate -->' \
  'This header guarantee is always available.' > docs/slots/model-draft.md
! python3 tools/check_docslots.py docs/slots
Enter fullscreen mode Exit fullscreen mode

Compile workflow in seven numbered steps

  1. Freeze the OpenAPI document and example transcripts under content-addressed filenames before drafting begins.
  2. Diff docslots.json against template.md and refuse the job if a heading lacks a slot identifier.
  3. Extract only the source kinds listed on each restate slot, such as parameters, status codes, and fixture bodies.
  4. Ask a model to fill restate and hybrid slots from those extracts, never from a blank page or prior chat.
  5. Run check_docslots.py and fail the pipeline on lexicon hits, forbid-heading leakage, or empty restatements.
  6. Route park and forbid slots to the named owner; do not merge until those bodies leave placeholder form.
  7. Publish the combined Markdown only after the validator and the human owners both pass.

The model prompt for step four should be mechanical and boring. Hand it the slot identifier, the allowed source excerpt, and a hard instruction to copy field names verbatim. Do not ask it to improve tone, infer defaults, or describe what happens if the service is down. Those inferences are commitments, and they belong in parked or forbidden slots.

Fill slot auth.headers (policy=restate).
Use only the JSON extract that follows.
Copy parameter names and types verbatim.
Do not mention availability, encryption, or support duration.
If a field is absent from the extract, write "not specified in schema".
Enter fullscreen mode Exit fullscreen mode

Where a free model and a free server fit

Restatement is a narrow transform: schema fragments in, constrained Markdown out, validator after. That job does not require a long-running private cluster if the extracts are already frozen in git. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host the restatement step and the slot validator without adding undocumented product claims to the compile graph. The published contract remains the slot map, the extracts, and the human-owned sections, not the drafting host.

Keep the validator on the same revision as the slot map. If the server only runs the model, a local or CI copy of check_docslots.py must still gate the merge. Hosting convenience should not become a second source of truth for heading policy.

What this workflow does not solve

The lexicon is a tripwire, not a semantics engine, and clever paraphrases can still smuggle a promise through. Hybrid walkthroughs remain the riskiest slots because narrative glue invites causal language that no status code can support. The validator also cannot tell whether an extract is stale, so a frozen spec that omitted a breaking change will produce a fluent lie. Teams still need a human read of park and forbid sections on every release that changes auth, pagination, or error codes.

This approach is a poor fit for narrative blogs, partner runbooks, or incident reviews where the value is judgment rather than recoverable shape. It is also a poor fit when the OpenAPI file is decorative and the real contract lives in server code. In that case, extract from tests or code first, then bind slots; do not launder an incomplete spec through a restatement model. Skip the workflow entirely if no owner will accept park and forbid sections, because an empty promise slot is worse than a missing heading.

Slot-typed headings make the division of labor visible in the repository instead of in a reviewer's head. Models restate what the extract can prove. Humans still write every sentence that tells a customer what the product will honor.

Top comments (0)