Generated reference tables stay honest when they compile from a hashed schema snapshot, not from chat memory of last week's routes. Chat-written parameter lists drift after a rename, and they often invent status codes the service never returns. The durable split is mechanical tables on one side and operational semantics on the other, with CI enforcing both. This article gives a snapshot file, a table compiler, an ownership manifest, and a check that fails dirty diffs.
Separate projection from meaning before any model runs
A parameter table is a projection of types, required flags, and enumerated values that already live in a reviewed schema. Failure semantics are not in that schema: they describe retries, partial commits, and which 409 is a quota event rather than a uniqueness clash. Mixing those layers in one prompt produces fluent pages that look complete while smuggling unverified operational claims into generated markdown. Teams that already pin examples to fixture hashes still leak meaning when a model rewrites what an error means in production.
The ownership boundary has to sit at the section, not only at the example block. Reference tables may be regenerated on every schema freeze. Production failure notes must remain human-owned files that the generator cannot open for write.
Inputs you freeze before the first draft
Do not start from a live OpenAPI URL during generation, because a moving document makes every table unverifiable later. Capture a snapshot, hash it, and treat that digest as the only legal input to the table compiler. Label the following as a proposed layout until your repository actually stores these paths.
- Write
docs/_facts/schema.snapshot.jsonfrom the reviewed spec, not from a model summary of the spec. - Record
docs/_facts/schema.snapshot.sha256in the same commit as the snapshot bytes themselves. - Keep
docs/reference/writable by the compiler and keepdocs/ops/unwritable by that same process. - Store
docs/_facts/ownership.yamlso continuous integration can reject a model diff that crosses ops paths.
The snapshot is a facts file for structure only. It must not contain retry advice, SLA sentences, or incident language that a human has not already signed elsewhere.
What the model may draft, and what it must not touch
Use the table below as a decision artifact during review, not as a vibe check after the pull request is already green. If a row would change customer behavior during an outage, it belongs on the human side even when the wording looks like a status table.
| Section | Source of truth | Author | Regenerable |
|---|---|---|---|
| Path list | Frozen snapshot | Compiler | Yes |
| Query and body parameter tables | Frozen snapshot | Compiler | Yes |
| Enum value tables | Frozen snapshot | Compiler | Yes |
| Documented HTTP status codes | Frozen snapshot responses map |
Compiler | Yes |
Which 409 is quota versus conflict |
Incident reviews, runbooks | Human | No |
| Retry, backoff, and idempotency rules | Platform contract | Human | No |
| Partial-failure and rollback notes | Storage and queue owners | Human | No |
| Support window and deprecation policy | Product and legal | Human | No |
A free drafting model can fill the regenerable rows if the compiler, not the chat transcript, is the publisher of record. It cannot be the publisher of the last four rows, because those claims are not mechanically present in the snapshot.
Artifact: snapshot, compiler, ownership file, and a failing check
The compiler below is labeled as a worked example. It reads a tiny frozen snapshot, emits markdown tables, and refuses to write under docs/ops/. Operators should extend field coverage rather than asking a model to invent columns the snapshot does not contain.
{
"snapshot_id": "payments-v3-2026-09-14",
"paths": {
"/v3/charges": {
"post": {
"operationId": "createCharge",
"parameters": [
{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}},
{"name": "dry_run", "in": "query", "required": false, "schema": {"type": "boolean"}}
],
"requestBody": {
"required": true,
"properties": {
"amount_cents": {"type": "integer", "minimum": 1},
"currency": {"type": "string", "enum": ["USD", "EUR"]}
}
},
"responses": {"201": {"description": "created"}, "409": {"description": "conflict"}}
}
}
}
}
# docs/_facts/ownership.yaml
compiler_write:
- docs/reference/parameters.md
- docs/reference/enums.md
- docs/reference/status-codes.md
human_only:
- docs/ops/failure-semantics.md
- docs/ops/retry-and-idempotency.md
- docs/ops/support-windows.md
snapshot: docs/_facts/schema.snapshot.json
snapshot_hash_file: docs/_facts/schema.snapshot.sha256
#!/usr/bin/env python3
"""Compile reference tables from a frozen schema snapshot. Do not write ops paths."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SNAP = ROOT / "docs/_facts/schema.snapshot.json"
HASH = ROOT / "docs/_facts/schema.snapshot.sha256"
REF = ROOT / "docs/reference"
FORBIDDEN_PREFIX = ROOT / "docs/ops"
def assert_hash() -> None:
digest = hashlib.sha256(SNAP.read_bytes()).hexdigest()
recorded = HASH.read_text(encoding="utf-8").strip()
if digest != recorded:
raise SystemExit(f"snapshot hash mismatch: {digest} != {recorded}")
def emit_parameters(spec: dict) -> str:
rows = ["| Operation | Name | In | Required | Type |", "| --- | --- | --- | --- | --- |"]
for path, item in spec["paths"].items():
for method, op in item.items():
op_id = op.get("operationId", f"{method.upper()} {path}")
for param in op.get("parameters", []):
schema = param.get("schema", {})
rows.append(
f"| {op_id} | `{param['name']}` | {param['in']} | "
f"{param.get('required', False)} | {schema.get('type', '')} |"
)
body = op.get("requestBody", {}).get("properties", {})
for name, schema in body.items():
required = name in op.get("requestBody", {}).get("required", []) or op.get("requestBody", {}).get("required") is True
rows.append(
f"| {op_id} | `{name}` | body | {bool(required)} | {schema.get('type', '')} |"
)
return "<!-- compiled from schema.snapshot.json; do not hand-edit -->\n" + "\n".join(rows) + "\n"
def emit_enums(spec: dict) -> str:
rows = ["| Field | Values |", "| --- | --- |"]
for item in spec["paths"].values():
for op in item.values():
for name, schema in op.get("requestBody", {}).get("properties", {}).items():
if "enum" in schema:
values = ", ".join(f"`{v}`" for v in schema["enum"])
rows.append(f"| `{name}` | {values} |")
return "<!-- compiled from schema.snapshot.json; do not hand-edit -->\n" + "\n".join(rows) + "\n"
def emit_statuses(spec: dict) -> str:
rows = ["| Operation | Status | Snapshot note |", "| --- | --- | --- |"]
for path, item in spec["paths"].items():
for method, op in item.items():
op_id = op.get("operationId", f"{method.upper()} {path}")
for code, body in op.get("responses", {}).items():
note = body.get("description", "")
rows.append(f"| {op_id} | `{code}` | {note} |")
return "<!-- compiled from schema.snapshot.json; do not hand-edit -->\n" + "\n".join(rows) + "\n"
def write_ref(name: str, text: str) -> None:
target = (REF / name).resolve()
if FORBIDDEN_PREFIX.resolve() in target.parents or target == FORBIDDEN_PREFIX.resolve():
raise SystemExit(f"refusing to write human-owned path: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding="utf-8")
def main() -> None:
assert_hash()
spec = json.loads(SNAP.read_text(encoding="utf-8"))
write_ref("parameters.md", emit_parameters(spec))
write_ref("enums.md", emit_enums(spec))
write_ref("status-codes.md", emit_statuses(spec))
if __name__ == "__main__":
main()
Human-owned failure notes stay in a separate file that the compiler never opens. The sample below is documentation structure, not a claim about any live payments API.
# Failure semantics (human-owned; do not generate)
The snapshot lists `409` as `conflict`. That token is not enough for operators.
- Treat `409` plus `code=idempotency_replay` as a safe replay, not as a unique-key failure.
- Treat `409` plus `code=quota_exhausted` as a shed load event; do not retry from the client SDK.
- A `201` does not mean the downstream ledger has committed; wait for `charge.settled`.
Retry and rollback rules live in retry-and-idempotency.md, not in the status table.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
expected="$(tr -d '[:space:]' < docs/_facts/schema.snapshot.sha256)"
actual="$(sha256sum docs/_facts/schema.snapshot.json | awk '{print $1}')"
if [[ "$expected" != "$actual" ]]; then
echo "schema snapshot hash mismatch" >&2
exit 1
fi
python3 tools/compile_reference_tables.py
git diff --name-only -- docs/ops | grep -q . && {
echo "compiler or model touched human-owned ops docs" >&2
exit 1
}
# Fail if compiled files lost their machine header after a free-model edit.
for f in docs/reference/parameters.md docs/reference/enums.md docs/reference/status-codes.md; do
grep -q "compiled from schema.snapshot.json" "$f" || exit 1
done
Numbered workflow for one schema change
Follow the sequence as a release checklist rather than as optional style guidance. Skipping the hash step is how invented columns re-enter the reference set.
- Review the schema change with the API owners, then freeze
schema.snapshot.jsonin a dedicated commit. - Recompute
schema.snapshot.sha256in that same commit so later compilers can refuse a swapped file. - Run
compile_reference_tables.pyand commit only files underdocs/reference/that the compiler emitted. - Update
docs/ops/failure-semantics.mdonly when production meaning changed, and require a human reviewer on that path. - Reject any model patch whose diff lists
docs/ops/or drops the compiled-from header on reference tables. - Publish the docs set only after both the snapshot hash job and the ops-path guard have passed.
A drafting model can propose table layout during step three if it never becomes the process that writes the markdown. The publisher of record remains the compiler against the hashed snapshot.
Where a free drafting server belongs in this split
Local compilation is enough when the snapshot is already reviewed and the table code is deterministic. Some teams still want a remote box to iterate on compiler prompts, header text, or column order without standing up their own GPU host. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that drafting loop while the repository continues to accept only compiler output under docs/reference/.
Do not send docs/ops/ into that loop, and do not treat model prose as a substitute for the snapshot hash. If the server is unavailable, the same compiler still runs on a laptop because the facts file is already in git. Keep promotion out of the ownership manifest; the manifest is a write-policy, not a product page.
Limitations the compiler will not hide
The snapshot cannot encode store-specific partial failure, because those behaviors are not HTTP response maps. Enum tables will omit values that exist only in a feature flag the schema never listed. Status tables will repeat the snapshot's one-line description, which is usually a token such as conflict, not an operator action. Hash pinning also fails closed if someone force-updates the snapshot without an API review, so the hash is a consistency check rather than a substitute for that review.
This workflow does not measure model quality, latency, or cost, and it does not claim a durability window for any hosted drafting server. It also does not generate getting-started walkthroughs, credential lifecycle copy, or signed tutorials, which remain separate human-owned surfaces.
Who should not use this split
Skip the compiler if you do not yet have a reviewed schema, because the tables would only freeze an untrusted draft. Skip it if legal requires every sentence in one signed narrative, because a generated table still needs a human attestation you may not be able to split. Skip it if your product has no mechanical responses map and every failure mode is tribal knowledge; write the runbook first, then decide whether tables are worth emitting. Autonomous doc bots that rewrite the whole docs/ tree in one pass should not adopt this pattern, because they erase the write boundary the checks exist to protect.
If you try the ownership manifest, review failure-semantics.md in the same change as the schema freeze rather than as a later polish pass.
Top comments (0)