DEV Community

Avery Lin
Avery Lin

Posted on

Compile Error-Code Catalogs From Source Enums; Keep Workaround Copy Human-Owned

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
python merge_overlay.py
pytest -q test_error_catalog.py
Enter fullscreen mode Exit fullscreen mode

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)