DEV Community

Avery Lin
Avery Lin

Posted on

Compile API Example Tables From Contract Fixtures Before Anyone Drafts Tutorial Prose

API docs go stale when writers retype request bodies that already exist as checked-in contract-test fixtures. The durable sequence is compile tables from those fixtures, draft tutorial glue, then hand-sign operational claims. A model may draft connective prose around compiled examples, but it must not invent authentication, retry, or rate-limit language. This workflow ships a small Python compiler, a claim-lane table, and a review gate that blocks unsigned operational sentences.

Why fixtures beat chat as the example source

Contract tests already store the request and response shapes your suite treats as correct, including status codes. Retyping those payloads into Markdown invites renamed fields, extra optional keys, and status codes rounded to 200. A compiler that reads the same JSON files the tests load cannot invent keys, and it can stamp source paths. Tutorial narrative still needs drafting, yet that narrative should wrap compiled tables rather than replace their contents.

Teams that skip this split usually discover the drift during an incident, not during review, because reviewers read prose faster than they diff JSON. A generated table with a fixture path is cheaper to audit than a freeform curl block copied from a chat transcript. The compiler does not make the documentation complete; it only makes the example lane mechanically honest. Completeness still depends on humans signing claims that no fixture file can prove.

Claim lanes: what a model may draft, and what a human must own

Treat every documentation sentence as belonging to one lane before a drafting session starts. The table below is a review contract, not a style preference, and unsigned cells must remain visible in the branch. Models are useful on the draft lane because wording can change without altering the wire contract. Humans own the sign lane because those sentences create operator obligations the test suite never asserted.

Lane Source of truth Model may draft? Human must own before publish
Endpoint identity Fixture meta.json (method, path) No, copy compiled values Confirm the path is public, not an internal alias
Request and response bodies request.json, response.json No, render tables only Confirm redaction of secrets and personal data
Status codes Fixture status integer No Confirm the code is the documented contract, not a test stub
Tutorial glue Compiled facts file Yes, surrounding prose only Edit for audience and remove invented fields
Authentication Absent from fixtures by design No Schemes, token lifetime, and required scopes
Idempotency Absent unless a key is in the fixture No Key header name, replay window, and conflict behavior
Retry and timeouts Not encoded in static JSON No Retryable statuses, backoff, and client timeout
Rate limits Not encoded in static JSON No Units, burst, and documented 429 body
Deprecation Git history or changelog process No Sunset date and replacement endpoint

If a fixture happens to include an Idempotency-Key header, still keep the replay window in the sign lane, because a single example cannot prove duration. The same rule applies to Authorization headers: the compiler should redact them, not describe the scheme. Tutorial glue may say that the next section shows a compiled create-order example. It may not say that clients should retry three times on 503.

Step 1: Freeze a fixture layout the compiler can walk

Keep one directory per example, and give every example three files with stable names the compiler can require. The layout below is a proposal for a public HTTP API; adjust names, but do not let examples live as unmarked blobs inside Markdown. Check the fixtures in beside the contract tests that load them, so a failing test and a stale table share one reviewable path.

fixtures/
  create-order/
    meta.json
    request.json
    response.json
  get-order/
    meta.json
    request.json
    response.json
Enter fullscreen mode Exit fullscreen mode

meta.json should carry only values the test already asserts, not marketing language. A minimal shape looks like the following example, which the compiler will copy into a heading and a method cell. Do not put retry advice, rate-limit numbers, or token lifetime into this file, because those claims would then look compiled when they are not.

{
  "id": "create-order",
  "title": "Create order",
  "method": "POST",
  "path": "/v1/orders",
  "status": 201,
  "content_type": "application/json"
}
Enter fullscreen mode Exit fullscreen mode

Store request and response bodies as pretty-printed JSON with sorted keys if your language allows it, so diffs stay small. If a field is a secret, replace it in the fixture with a stable placeholder such as "<redacted-token>" before the file is committed. The compiler should refuse to emit a table when it sees high-entropy values that look like live credentials, which is a mechanical check, not a complete secret scanner.

Step 2: Run a compiler that emits tables and UNSIGNED markers

The script below is a labeled, unexecuted example: read it as a starting compiler, not as production metrics from a live corpus. It walks fixtures/, writes docs/generated/examples.md, and appends an UNSIGNED claim block that publishing jobs must still see. Run it from the repository root so source paths in the Markdown match paths reviewers can open.

#!/usr/bin/env python3
"""Compile fixture directories into Markdown example tables.

Proposal / unexecuted example: this script is a reviewable artifact,
not a report of production runtime or coverage.
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

ROOT = Path(".")
FIXTURES = ROOT / "fixtures"
OUT = ROOT / "docs" / "generated" / "examples.md"
SECRETISH = re.compile(r"(?i)(api[_-]?key|secret|token|password|authorization)")

UNSIGNED_BLOCK = """
## UNSIGNED operational claims (do not publish while any line remains)

- [ ] Authentication scheme, scopes, and token lifetime
- [ ] Idempotency key header, replay window, and conflict status
- [ ] Retryable statuses, backoff policy, and client timeout
- [ ] Rate-limit unit, burst, and 429 body
- [ ] Deprecation or sunset notes, if this example is leaving the contract
"""


def die(msg: str) -> None:
    sys.stderr.write(msg + "\n")
    raise SystemExit(1)


def load_json(path: Path) -> object:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        die(f"missing {path}")
    except json.JSONDecodeError as exc:
        die(f"invalid JSON in {path}: {exc}")


def assert_no_live_secrets(blob: object, path: Path) -> None:
    text = json.dumps(blob)
    if SECRETISH.search(text) and "<redacted" not in text.lower():
        die(f"possible live secret in {path}; redact before compiling")


def fence(obj: object) -> str:
    return "```

json\n" + json.dumps(obj, indent=2, sort_keys=True) + "\n

```"


def compile_one(dirpath: Path) -> str:
    meta = load_json(dirpath / "meta.json")
    request = load_json(dirpath / "request.json")
    response = load_json(dirpath / "response.json")
    if not isinstance(meta, dict):
        die(f"{dirpath}/meta.json must be an object")
    for key in ("id", "title", "method", "path", "status"):
        if key not in meta:
            die(f"{dirpath}/meta.json missing {key}")
    assert_no_live_secrets(request, dirpath / "request.json")
    assert_no_live_secrets(response, dirpath / "response.json")
    rel = dirpath.as_posix()
    return "\n".join(
        [
            f"### {meta['title']}",
            "",
            f"Compiled from `{rel}`. Do not edit this table by hand.",
            "",
            f"- Method: `{meta['method']}`",
            f"- Path: `{meta['path']}`",
            f"- Status: `{meta['status']}`",
            "",
            "Request body:",
            fence(request),
            "",
            "Response body:",
            fence(response),
            "",
        ]
    )


def main() -> None:
    if not FIXTURES.is_dir():
        die("fixtures/ directory is required")
    parts = [
        "# Compiled API examples",
        "",
        "This file is generated from fixtures/. Edit fixtures, not this file.",
        "Tutorial prose belongs in a separate Markdown file that links here.",
        "",
    ]
    dirs = sorted(p for p in FIXTURES.iterdir() if p.is_dir())
    if not dirs:
        die("no fixture directories found")
    for dirpath in dirs:
        parts.append(compile_one(dirpath))
    parts.append(UNSIGNED_BLOCK.strip())
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text("\n".join(parts) + "\n", encoding="utf-8")
    print(f"wrote {OUT}")


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

A typical local run is one command, followed by a git diff that should touch only docs/generated/examples.md when fixtures change. If the diff also rewrites tutorial files, the draft lane and the compile lane have been mixed, and the review should stop. Keep the compiler deterministic: sorted keys, stable directory order, and no timestamps in the output file.

python3 compile_examples.py
git diff -- docs/generated/examples.md
Enter fullscreen mode Exit fullscreen mode

Step 3: Draft tutorial glue only from the compiled facts file

Create a second Markdown file such as docs/create-order.md that links to generated tables instead of inlining a second copy of the JSON. The drafting session, whether human or model, should receive the compiled file as read-only context and a short instruction that UNSIGNED sections are out of scope. Paste the generated tables, not the raw fixture tree, so the model cannot “helpfully” restore a redacted token from an earlier chat turn.

The prompt belongs in the repository as a checklist, not as folklore in a chat window. The following block is a proposal you can store as docs/DRAFTING.md and require authors to follow. Notice that it asks for connective prose and forbids filling operational claims, which keeps the sign lane empty until a human writes it.

You are drafting tutorial glue for an HTTP API.
Read docs/generated/examples.md as the only source of payloads.
Write a short narrative that points at the compiled Create order table.
Do not invent fields that are missing from the compiled JSON.
Do not write authentication, idempotency, retry, timeout, or rate-limit sentences.
Do not remove or complete any UNSIGNED checklist item.
If a reader would need an operational claim, insert TODO(HUMAN) and stop.
Enter fullscreen mode Exit fullscreen mode

If you want a remote shell for the compiler and a separate session for narrative glue, MonkeyCode currently offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Use that session only against the compiled facts file, and leave every UNSIGNED line untouched until a human reviewer writes the sign-lane sentences.

After drafting, run a cheap textual check that tutorial files do not contain JSON objects duplicating generated tables. Duplication is how the compile lane silently forks. A one-line grep for a distinctive compiled field name in docs/*.md excluding docs/generated/ is usually enough to catch the fork before review.

Step 4: Hand-sign operational claims in a separate, reviewed file

Put signed claims in docs/signed/operations.md, and require a human reviewer who owns the API contract, not only the documentation style. Each signed paragraph should cite evidence: an identity-provider doc, a gateway config, or a runbook, rather than a model’s prior wording. If evidence is missing, the claim stays UNSIGNED and the example can still ship as a table without operator promises.

A signed idempotency paragraph should name the header, the replay window, and the status returned on a payload conflict. A signed retry paragraph should name which statuses are retryable and which are not, plus the client timeout the vendor will not extend ad hoc. A signed rate-limit paragraph should name the unit and the 429 body shape, because clients parse bodies more often than they parse marketing prose. None of those sentences belong in meta.json.

## Create order — signed operations

Evidence: `gateway/routes/orders.yaml` reviewed in PR 1842.

- Authentication: bearer access token, scope `orders:write`.
- Idempotency: header `Idempotency-Key`, replay window 24 hours, conflict `409`.
- Retry: retry `429` and `503` only; do not retry `400` or `409`.
- Timeout: clients must fail the call after 10 seconds.
- Rate limit: 100 requests per minute per token; `429` body includes `retry_after`.
Enter fullscreen mode Exit fullscreen mode

The numbers in that sample are placeholders for your own reviewed evidence, not measurements claimed by this article. Replace them only when a human can point at a config file or an SLA document in the same change. If the gateway file and the signed paragraph disagree, fail the review even when the compiled tables are correct, because operators will trust the signed file.

Step 5: Gate publishing on remaining UNSIGNED tokens

Add a CI step that fails when docs/generated/examples.md still contains the UNSIGNED heading at release time, or when tutorial files contain TODO(HUMAN). The check is intentionally blunt: string presence is enough, because the compiler always emits the heading until you delete it after signing. Do not teach the model to delete the heading; deletion is a human publish action.

#!/bin/sh
set -eu
if grep -q "UNSIGNED operational claims" docs/generated/examples.md; then
  echo "docs still carry UNSIGNED claims; refuse publish" >&2
  exit 1
fi
if grep -R "TODO(HUMAN)" docs --include='*.md'; then
  echo "tutorial glue still has TODO(HUMAN) markers" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Keep this job on the tag or docs-publish pipeline rather than on every draft pull request, so authors can still iterate with visible UNSIGNED markers. Draft pull requests should prove the opposite invariant: the generated file must contain the UNSIGNED block after a clean compile. That two-phase check stops both premature publishing and accidental deletion of the safety net.

Limitations

The compiler only knows what the fixture directory encoded, so missing examples stay missing rather than being inferred from types. It will not detect semantic drift when a test stub returns 201 and production returns 202, unless that stub is the committed fixture. Redaction is a regular-expression heuristic and will miss secrets that use bland key names, so a real secret scanner still belongs in the repository. Narrative quality remains a human editing problem; the draft lane can be fluent and still wrong about audience or version.

This workflow also does not generate architecture diagrams, SDK method matrices, or changelog classification. If your contract tests use binary bodies or multipart uploads, the JSON-only compiler will need a different renderer, and that renderer should still refuse unsigned operational claims. Multi-language docs need one compiled facts file and per-language glue files, not three independently rewritten payload tables.

Who should not use this approach

Skip this compiler if you do not have contract tests or checked-in fixtures, because the method has no source of truth besides chat. Skip it if the product is a private prototype whose examples change hourly and nobody yet owns authentication policy. Skip it if legal or compliance requires every documentation sentence to be written by a named employee with no model in the loop; the draft lane would violate that constraint. Skip it if your “fixtures” are production traffic dumps, because those dumps mix personal data into the compile lane.

Teams that already generate OpenAPI artifacts can still use the claim-lane table without the Python script, provided examples are rendered from the spec and operational claims stay in a signed file. The important split is not Python versus an OpenAPI toolchain. The important split is compiled examples versus signed operator obligations.

Closing

Ship example tables from the same JSON your contract tests already trust, then draft tutorial glue as a thin wrapper around those tables. Keep authentication, idempotency, retry, rate limits, and deprecation in a human-signed file with evidence paths a reviewer can open. If you try the compiler, leave every UNSIGNED line empty until that evidence exists, and treat deletion of the heading as a publish decision rather than a drafting convenience.

Top comments (0)