Schema examples can be compiled into fixture files because they already live beside the operations they describe. Canonical walkthroughs cannot be compiled because they name environments, languages, and success criteria that the spec never proves. A documentation pipeline that lets a model draft both will publish copy-paste recipes that imply a live stack nobody signed. Split the work: generate fixtures from OpenAPI, then require a human signature on every getting-started path.
Fixtures compile; walkthroughs name a world the spec does not prove
An OpenAPI document already carries operation identifiers, HTTP methods, paths, media types, and optional example objects. Those fields are closed under the spec, so a compiler can emit fixtures without inventing a customer narrative or a live host. Walkthroughs are not closed under the spec, because they choose a base URL and a supported language. They also choose a definition of done that readers will treat as an operational promise.
This distinction is a documentation-control problem, rather than a complaint about the writing quality of generated prose. Fluent recipes fail in onboarding because they smuggle environment claims into pages that were never reviewed as environment claims. Keep the model on fixture commentary after compilation, and keep every copy-paste path behind a human signature block.
OpenAPI 3.x example and examples objects remain the right primary source for bodies, headers, and media types. The OpenAPI Specification defines those objects as part of the document, not as a tutorial voice. Current claims about what the compiler may emit should be checked against that spec text, not against a chat transcript.
Decision table for a docs-generation board
Use a board-readable table so reviewers do not debate style when the real question is ownership. If a cell in the human column is empty at review time, the page does not ship. Models do not get a vote on empty ownership cells, even when the prose is fluent and locally consistent.
| Artifact | Evidence already in the repo | Model may draft | Human must own before publish |
|---|---|---|---|
| Request and response fixture bodies | OpenAPI example or examples
|
Commentary that restates schema field descriptions | Whether that fixture is the canonical reader sample |
| Operation inventory |
paths plus operationId
|
Grouping labels for the inventory file | Stability badges and the start-here order |
curl skeletons with placeholders |
Method, path, required parameters | Alternate skeletons in unsigned scratch files | Base URL, auth scheme in use, and target environment |
| Getting-started walkthrough | None by itself | Outline headings only, never runnable hosts | Supported languages, success criteria, and signature |
| Error body shapes | Documented error schemas | Field-by-field restatements of those schemas | Retry advice, outage language, and customer-facing severity |
| PII and secret posture | None | A checklist of patterns to inspect | The redaction policy and any production-like values |
The inventory row is easy to automate, and it is still easy to over-claim in the same file. A compiler may list every operationId; it may not mark which three operations a new reader should run first. Ordering, badges, and start-here labels stay with the humans who own onboarding outcomes for that API. Error shapes can be copied from schemas, but retry guidance is a product promise and needs a name on the page.
A six-step workflow that keeps the files apart
The following sequence is a proposed control path for a docs repository that already stores OpenAPI JSON. It is labeled as a proposal because it is not presented here with production incident data. Each step produces a file that CI can hash, including the unsigned walkthrough template that later gains a signature.
1. Freeze the OpenAPI document as the only fixture input
Pin a reviewed OpenAPI 3.x JSON file in the docs repository and refuse chat paste as compiler input. Treat $ref resolution as a compile step that must finish before any model sees the document. If an operation lacks an example, emit an empty fixture with provenance: missing_example instead of asking a model to invent a body. Record the spec git hash next to the fixture manifest so reviewers can see which document produced the files.
2. Compile fixtures with a deterministic script
Run a small compiler that walks paths, reads example or the first examples entry, and writes one JSON file per operation. The compiler must not call a model, and it must not consult README prose. It should fail loudly when the spec is not JSON, when paths is missing, or when a media type object is malformed. Proposed local command:
python3 compile_fixtures.py --spec specs/openapi.json --out fixtures/
sha256sum specs/openapi.json > fixtures/SPEC.sha256
3. Mark provenance and run a boring PII scan
Every fixture carries provenance: schema_example or provenance: missing_example so later diffs stay explainable. A second command scans fixture values for email-like strings, bearer-shaped tokens, and long digit runs that look like account numbers. Hits do not auto-redact; they fail the job so a human chooses a synthetic replacement. That failure is the point of the scan, because silent redaction hides the ownership decision.
4. Allow a model to draft fixture commentary only
After fixtures exist on disk, a model may draft a sibling commentary.md that restates schema descriptions next to field names. The prompt must receive the compiled fixture and the schema slice, not the walkthrough template and not a live hostname. Teams that want a hosted place to run that narrow pass can use MonkeyCode's free model access and free server option for the commentary job. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The commentary file remains unsigned documentation and cannot include a host, an SDK language, or a time-to-success claim. If the model adds a curl against a real-looking hostname, the review bot rejects the file. Free-model output is cheap to regenerate, which is useful only when the JSON bodies were already compiled from the spec.
5. Open every walkthrough with an empty signature
Getting-started pages start as a template whose frontmatter is invalid until a human fills it. The template lists the fixture files it may cite, and it forbids inline JSON that does not match a compiled fixture. Reviewers compare the walkthrough request body to the fixture using a structural diff, not a prose skim. A page that adds a field the fixture does not contain is treated as a spec change, not as a writing fix.
6. Fail CI when a walkthrough is missing a signature or a base URL
A final check reads Markdown frontmatter and rejects pages under walkthroughs/ when signed_by, base_url, or supported_languages is empty. It also rejects localhost if the page claims a shared team environment, and it rejects language lists that are longer than the languages in the signed SDK matrix file. The check is a gate on publish, not a grammar linter.
Proposed compiler and sample spec
The OpenAPI snippet and Python script below are a proposed, locally runnable example. They are not production measurements, they do not call a network, and they do not name a hosted model. Save the spec as specs/openapi.json.
{
"openapi": "3.0.3",
"info": {"title": "Widgets", "version": "1.2.0"},
"paths": {
"/widgets": {
"post": {
"operationId": "createWidget",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string", "description": "Display name for the widget."}
}
},
"example": {"name": "sample-widget"}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"example": {
"id": "00000000-0000-4000-8000-000000000001",
"name": "sample-widget"
}
}
}
}
}
}
}
}
}
#!/usr/bin/env python3
"""Proposed compiler: OpenAPI JSON -> per-operation fixture JSON. No network."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
EMAIL = re.compile(r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", re.I)
BEARER = re.compile(r"Bearer\s+[A-Za-z0-9._\-]{12,}", re.I)
LONG_DIGITS = re.compile(r"\b\d{12,}\b")
def pick_example(media_obj):
if not isinstance(media_obj, dict):
return None, "malformed_media"
if "example" in media_obj:
return media_obj["example"], "schema_example"
examples = media_obj.get("examples") or {}
if isinstance(examples, dict) and examples:
first = next(iter(examples.values()))
if isinstance(first, dict) and "value" in first:
return first["value"], "schema_example"
return first, "schema_example"
return None, "missing_example"
def first_content(obj):
content = (obj or {}).get("content") or {}
if not content:
return None, None
media_type, media_obj = next(iter(content.items()))
return media_type, media_obj
def scan_pii(value, hits):
blob = json.dumps(value, sort_keys=True)
if EMAIL.search(blob):
hits.append("email_like")
if BEARER.search(blob):
hits.append("bearer_like")
if LONG_DIGITS.search(blob):
hits.append("long_digit_run")
def compile_spec(spec):
fixtures = []
paths = spec.get("paths") or {}
if not isinstance(paths, dict) or not paths:
raise SystemExit("spec is missing paths")
for path, item in paths.items():
if not isinstance(item, dict):
continue
for method, op in item.items():
if method.startswith("x-") or method == "parameters":
continue
if not isinstance(op, dict):
continue
op_id = op.get("operationId") or f"{method}_{path.strip('/').replace('/', '_')}"
req_type, req_media = first_content(op.get("requestBody"))
req_body, req_prov = pick_example(req_media) if req_media else (None, "missing_example")
responses = op.get("responses") or {}
resp_entry = None
for code in ("201", "200"):
if code in responses:
resp_entry = (code, responses[code])
break
if resp_entry is None and responses:
code = next(iter(responses))
resp_entry = (code, responses[code])
resp_type, resp_media = first_content(resp_entry[1] if resp_entry else None)
resp_body, resp_prov = pick_example(resp_media) if resp_media else (None, "missing_example")
fixture = {
"operation_id": op_id,
"method": method.upper(),
"path": path,
"request": {
"content_type": req_type,
"body": req_body,
"provenance": req_prov,
},
"response": {
"status": resp_entry[0] if resp_entry else None,
"content_type": resp_type,
"body": resp_body,
"provenance": resp_prov,
},
}
hits = []
scan_pii(req_body, hits)
scan_pii(resp_body, hits)
fixture["pii_hits"] = sorted(set(hits))
fixtures.append(fixture)
return fixtures
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--spec", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args()
spec = json.loads(Path(args.spec).read_text(encoding="utf-8"))
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
fixtures = compile_spec(spec)
blocked = [f for f in fixtures if f["pii_hits"]]
for fixture in fixtures:
name = f"{fixture['operation_id']}.json"
(out / name).write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
manifest = {"count": len(fixtures), "operations": [f["operation_id"] for f in fixtures]}
(out / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
if blocked:
print("pii gate failed:", file=sys.stderr)
for fixture in blocked:
print(f"{fixture['operation_id']}: {fixture['pii_hits']}", file=sys.stderr)
raise SystemExit(2)
print(f"wrote {len(fixtures)} fixtures to {out}")
if __name__ == "__main__":
main()
Expected fixture body for the sample, with provenance taken from the spec example rather than from generated prose:
{
"operation_id": "createWidget",
"method": "POST",
"path": "/widgets",
"request": {
"content_type": "application/json",
"body": {"name": "sample-widget"},
"provenance": "schema_example"
},
"response": {
"status": "201",
"content_type": "application/json",
"body": {
"id": "00000000-0000-4000-8000-000000000001",
"name": "sample-widget"
},
"provenance": "schema_example"
},
"pii_hits": []
}
Walkthrough pages start from a template that is invalid on purpose. Humans fill the ownership fields; the model does not.
---
requires_signature: true
fixture_refs:
- fixtures/createWidget.json
base_url:
supported_languages: []
success_criteria:
signed_by:
signed_at:
---
# Create a widget
Cite `fixtures/createWidget.json` for the request body.
Do not paste a host until `base_url` is signed.
Humans also maintain a small SDK matrix that CI will compare against walkthrough language lists. The matrix is not a model input.
{
"supported_languages": ["python", "typescript"],
"unsigned_languages": ["go", "java", "ruby"]
}
Review commands for CI
These commands are a proposed gate set, not a hosted product test. Run them after the compiler and before merge.
python3 compile_fixtures.py --spec specs/openapi.json --out fixtures/
python3 - <<'PY'
import json, sys, pathlib
try:
import yaml
except ImportError:
yaml = None
def load_front(path):
text = path.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
_, raw, _ = text.split("---", 2)
if yaml:
return yaml.safe_load(raw) or {}
data = {}
for line in raw.splitlines():
if ":" in line:
k, v = line.split(":", 1)
data[k.strip()] = v.strip()
return data
matrix = json.loads(pathlib.Path("sdk_matrix.json").read_text())
allowed = set(matrix["supported_languages"])
failed = 0
for path in pathlib.Path("walkthroughs").glob("*.md"):
meta = load_front(path)
missing = [k for k in ("signed_by", "base_url", "supported_languages", "success_criteria") if not meta.get(k)]
langs = meta.get("supported_languages") or []
if isinstance(langs, str):
langs = [s.strip() for s in langs.strip("[]").split(",") if s.strip()]
extra = [l for l in langs if l not in allowed]
if missing or extra or str(meta.get("base_url", "")).endswith("localhost"):
print(f"REJECT {path}: missing={missing} extra_languages={extra}")
failed = 1
refs = meta.get("fixture_refs") or []
for ref in refs:
if not pathlib.Path(str(ref)).exists():
print(f"REJECT {path}: missing fixture {ref}")
failed = 1
sys.exit(failed)
PY
Commentary files need a separate reject rule. Proposed grep, which is intentionally crude:
if grep -ERi 'https?://|curl |npm install|pip install' commentary/; then
echo "commentary cited a host or installer; move that claim to a signed walkthrough"
exit 1
fi
Limitations
This workflow does not prove that an example validates against its schema; pair it with a real OpenAPI validator if you need that guarantee. It does not classify breaking changes, support windows, or failure contracts, which belong in other signed documents. Free-model commentary can still misstate a field even when the JSON body was compiled correctly, so reviewers read commentary as a draft. The PII scan is a pattern gate, not a compliance program, and it will miss encoded secrets and novel identifier formats.
The approach also assumes a single reviewed spec file is allowed to be the fixture source of truth. Multi-spec gateways, vendor overlays, and unpublished partner APIs will need an extra freeze step before compilation. Do not treat a green CI signature check as legal review. Do not treat a UUID in a sample as a uniqueness guarantee, because the compiler only copied the spec example.
Who should skip this split: teams that do not yet have a reviewed OpenAPI document and are still designing the API in slides. Skip it if every page is a contract that legal must approve line by line, because a fixture compiler will not shorten that queue. Skip it if the walkthrough is an executable notebook that hits production data; those runs need a separate safety review. Skip it if you need the model to invent missing examples, because that reintroduces unsigned bodies.
Documentation repositories that already generate field tables or claim lists can add this workflow beside those pipelines without merging the artifacts. Fixtures remain compilable files. Walkthroughs remain signed recipes. The model stays on commentary, which is the only place fluent prose is cheap enough to regenerate on every spec change.
If the repository already compiles fixtures in CI, keep walkthrough signatures in human review and regenerate only the commentary files.
Top comments (0)