Generated error catalogs should come from the same enums the service returns, not from a chat transcript. Models may draft compact technical meanings, while humans own workarounds, severity labels, and public incident language. Continuous integration should fail if a generated page rewrites overlay fields or if overlay keys cite codes the enum no longer emits. The workflow below is a compiler plus a closed test, not a prompt you paste into an editor and hope remains true.
The failure mode this compiler is built to stop
Public error pages drift in a predictable way when teams ask a model to rewrite the whole troubleshooting chapter. Symbolic names change in code, while the published table still lists retired codes and omits new ones that clients already receive. Workaround paragraphs then invent retry advice that contradicts the transport layer, because the model never saw the timeout budget or the idempotency rules. Status-page severity also leaks into generated files, which makes a later incident doc disagree with the catalog that support already quoted.
A catalog compiler does not solve product policy. It only keeps the mechanical columns synchronized with the enum that the process actually serializes. Policy columns stay in a human overlay that the compiler may read and must never write. That split is the whole design; everything else is packaging for review.
Ownership decision table
Use this table as the contract for generated pages. If a column is not in the table, treat it as human-owned until a review adds it.
| Column | Source of truth | Model may draft? | Human must own? | CI rule |
|---|---|---|---|---|
code (numeric or string) |
Source enum / registry | No | No; extracted only | Must match enum members exactly |
symbol |
Source enum name | No | No; extracted only | Rename in code, not in Markdown |
http_status |
Mapping beside the enum | No | Confirm mapping in review | Fail on duplicate code with two statuses |
meaning (one sentence, technical) |
Comment on the enum member | Yes, from that comment only | Edit the comment, then regenerate | Reject meanings that cite SLAs or money |
workaround |
Overlay file | No | Yes | Generated files must not contain this key |
severity_public |
Overlay file | No | Yes | Ban words like outage, breach, refund |
retry_hint |
Overlay file | No | Yes | Must not contradict Idempotency-Key docs |
status_page_blurb |
Overlay file | No | Yes | Overlay-only path; unsigned drafts cannot touch it |
The table is the artifact you review in pull requests. Do not let a model add columns during a draft pass, because new columns become a silent expansion of machine authority. If you need a new column, add it to this table in the same change that teaches the compiler about it.
What the model may draft, and what it must not touch
The model may propose a one-sentence meaning when an enum comment exists and the sentence stays inside that comment’s claims. It may also reflow the generated Markdown table so numeric codes sort stably and unused symbols disappear after a delete. It must not invent workarounds, name vendors, promise refunds, or describe data deletion. It must not write HTTP statuses that are absent from the mapping table beside the enum.
Those restrictions are boring on purpose. Error catalogs fail in production when generated prose starts making operational promises. Keep the model on extraction, sorting, and comment compression. Leave every sentence that a customer might quote during an incident inside the overlay, with a human signature on the pull request.
Workflow
Follow these numbered steps in order. Skipping the freeze step is how catalogs start quoting yesterday’s enum.
1. Freeze the enum the service actually returns
Put error codes in one module that the HTTP layer imports. Do not keep a second list in documentation configuration, because the second list will win in review and lose in production. The sample below is labeled as a worked example, not as a harvested production dump.
# errors.py — example registry the HTTP layer should import
from enum import Enum
class ApiError(Enum):
# meaning: the JSON body was well-formed but failed schema validation
INVALID_BODY = ("invalid_body", 400)
# meaning: the Idempotency-Key header was missing on a mutating route
IDEMPOTENCY_REQUIRED = ("idempotency_required", 400)
# meaning: the resource exists but the caller cannot read or mutate it
NOT_AUTHORIZED = ("not_authorized", 403)
# meaning: no row matched the path identifier after authorization
NOT_FOUND = ("not_found", 404)
# meaning: a conflicting write arrived with the same idempotency key
CONFLICT = ("conflict", 409)
# meaning: an upstream dependency timed out before a local commit
UPSTREAM_TIMEOUT = ("upstream_timeout", 504)
def __init__(self, symbol: str, http_status: int) -> None:
self.symbol = symbol
self.http_status = http_status
Export a JSON snapshot in CI so documentation jobs do not import application code at render time if that import is heavy. The snapshot is the freeze; chat output is not.
python - <<'PY'
import json, inspect, errors
from pathlib import Path
rows = []
for member in errors.ApiError:
comment = inspect.getdoc(errors.ApiError) # class doc only; member comments need a small AST pass
rows.append({
"code": member.symbol,
"symbol": member.name,
"http_status": member.http_status,
})
Path("artifacts/error_enum.freeze.json").write_text(json.dumps(rows, indent=2, sort_keys=True))
print("wrote artifacts/error_enum.freeze.json")
PY
Member comments in the example live above each assignment, so a real extractor should use the ast module rather than inspect.getdoc. The freeze file is still the input to every later step, which keeps draft passes from reading a dirty working tree.
2. Extract mechanical columns with a compiler, not a prompt
The extractor below reads the freeze file and optional source comments, then writes only the columns the decision table marks as generated. Treat it as a starting implementation you can extend; it is not a benchmarked product component.
# compile_error_catalog.py
from __future__ import annotations
import ast
import json
from pathlib import Path
GENERATED_KEYS = ("code", "symbol", "http_status", "meaning")
BANNED_IN_MEANING = ("sla", "refund", "outage", "breach", "compensation")
def member_comments(source: str) -> dict[str, str]:
tree = ast.parse(source)
comments: dict[str, str] = {}
lines = source.splitlines()
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
for stmt in node.body:
if not isinstance(stmt, ast.Assign):
continue
if not stmt.targets or not isinstance(stmt.targets[0], ast.Name):
continue
name = stmt.targets[0].id
idx = stmt.lineno - 2
bits = []
while idx >= 0 and lines[idx].strip().startswith("#"):
bits.append(lines[idx].split("#", 1)[1].strip())
idx -= 1
if bits:
text = " ".join(reversed(bits))
if text.lower().startswith("meaning:"):
text = text.split(":", 1)[1].strip()
comments[name] = text
return comments
def compile_rows(freeze_path: Path, source_path: Path) -> list[dict]:
freeze = json.loads(freeze_path.read_text())
comments = member_comments(source_path.read_text())
rows = []
for item in freeze:
meaning = comments.get(item["symbol"], "")
lowered = meaning.lower()
if any(word in lowered for word in BANNED_IN_MEANING):
raise SystemExit(f"meaning for {item['symbol']} contains policy language")
rows.append({
"code": item["code"],
"symbol": item["symbol"],
"http_status": item["http_status"],
"meaning": meaning,
})
rows.sort(key=lambda r: (r["http_status"], r["code"]))
return rows
def to_markdown(rows: list[dict]) -> str:
lines = [
"<!-- generated: do not edit; overlay lives in errors.overlay.yaml -->",
"| code | symbol | http_status | meaning |",
"| --- | --- | --- | --- |",
]
for row in rows:
lines.append(
f"| `{row['code']}` | `{row['symbol']}` | {row['http_status']} | {row['meaning']} |"
)
return "\n".join(lines) + "\n"
if __name__ == "__main__":
rows = compile_rows(Path("artifacts/error_enum.freeze.json"), Path("errors.py"))
Path("docs/reference/errors.generated.md").write_text(to_markdown(rows))
Run it as a command that CI can replay without a network.
mkdir -p artifacts docs/reference
python compile_error_catalog.py
git diff --check docs/reference/errors.generated.md
3. Optional draft pass for missing meanings only
If a member has no comment, a draft pass may propose a single technical sentence from the symbol name and the HTTP status, then write that sentence back into the source comment. It must not write docs/policy/ or the overlay file. After the comment exists, regenerate the catalog so the published table still comes from the compiler.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. A draft pass that only fills missing comments can run against MonkeyCode’s free model access on the free server option, because the job does not need a private fine-tune or a long-lived GPU claim. Keep the overlay off that machine’s writable paths, and merge overlay edits only through the same review as any other policy change. Do not paste customer incident notes into the draft prompt; the compiler has no need for them.
Label every drafted comment as a proposal until a reviewer accepts it in the enum file. A useful review question is whether the sentence would still be true if the HTTP status changed. If the answer depends on a product promise, the sentence belongs in the overlay, not in meaning.
4. Keep workaround and severity copy in a human overlay
The overlay is YAML keyed by code. Humans add fields the compiler is forbidden to emit. Missing overlay keys are allowed for brand-new codes, but publishing should fail if you require public workarounds for every 5xx.
# errors.overlay.yaml — human-owned; compiler must not write this file
invalid_body:
workaround: "Fix the request body against the published JSON Schema and retry."
severity_public: "request_error"
retry_hint: "safe_to_retry_after_fix"
not_authorized:
workaround: "Check the token audience and the resource ACL; do not retry blindly."
severity_public: "request_error"
retry_hint: "do_not_retry_until_grants_change"
upstream_timeout:
workaround: "Retry only when the original request carried a stable Idempotency-Key."
severity_public: "degraded_dependency"
retry_hint: "retry_with_same_idempotency_key"
A tiny merge step can print a support view without copying overlay text into the generated reference page. Keep the public reference mechanical, and attach workarounds in a signed support article if you need them on the same site.
# merge_overlay.py
import json, sys, yaml
from pathlib import Path
overlay = yaml.safe_load(Path("errors.overlay.yaml").read_text()) or {}
freeze = json.loads(Path("artifacts/error_enum.freeze.json").read_text())
codes = {row["code"] for row in freeze}
unknown = sorted(set(overlay) - codes)
if unknown:
sys.exit(f"overlay cites codes missing from enum: {unknown}")
print("overlay keys match the frozen enum")
5. Fail closed in CI
The test plan is short and should stay short. Generated Markdown must not contain overlay field names. Overlay keys must be a subset of frozen codes. Meanings must not contain the banned policy words. The generated file header must keep the do-not-edit marker so reviewers do not “fix” a table by hand.
# test_error_catalog.py
from pathlib import Path
OVERLAY_KEYS = ("workaround", "severity_public", "retry_hint", "status_page_blurb")
def test_generated_page_stays_mechanical():
text = Path("docs/reference/errors.generated.md").read_text().lower()
assert text.startswith("<!-- generated")
for key in OVERLAY_KEYS:
assert key not in text
def test_overlay_codes_exist_in_freeze(tmp_path=None):
import json, yaml
overlay = yaml.safe_load(Path("errors.overlay.yaml").read_text()) or {}
freeze = {row["code"] for row in json.loads(Path("artifacts/error_enum.freeze.json").read_text())}
assert set(overlay).issubset(freeze)
python merge_overlay.py
pytest -q test_error_catalog.py
Wire both commands to the documentation pipeline that already builds the site. If the site generator can overwrite errors.generated.md from a template cache, point it at the compiler output and disable template edits for that path.
Limitations
This approach assumes one enum, or a small set of enums, is the complete public error surface. It will lie if handlers return ad hoc strings that never pass through the registry. Comment parsing is whitespace-sensitive and will miss meanings that live in unstructured tickets. The banned-word list is a tripwire, not a legal review, and it will not catch a polite paragraph that still promises compensation. Overlay YAML also does not replace incident communications; it only stops the generated catalog from becoming an unofficial status page.
The draft pass does not verify that a proposed meaning matches runtime behavior. Only tests against the real handler, or a recorded cassette you already trust, can do that. If your errors are localized, generate one mechanical table per locale from the same freeze, and keep legal translations in a separate human-owned tree.
Who should not use this
Do not use this compiler if your public errors are free-form English with no stable codes. Do not use it to generate security advisories, breach notices, or refund policy, because those documents need counsel and a named signer. Do not use a draft pass as a substitute for deleting retired codes from the enum; the freeze will keep publishing them until the source changes. Teams that cannot run CI on documentation paths should not adopt the split, because the overlay will rot without a gate.
If a reviewer cannot tell which columns were compiled, the page is already unsafe to ship. Keep the generated table dull, keep the overlay signed, and treat every new column as a change to the decision table rather than a clever prompt.
Top comments (0)