Generated API documentation fails most often when compile work and signature work share one prompt. A model can restate schemas, examples, and error catalogs when those artifacts already exist in the repository. A human must still own deprecation windows, support scope, and any sentence that binds the product to a future. This article defines a draft-lane workflow, a decision table, and a small classifier you can run before merge.
Separate restatement from obligation
Reviewers treat a generated page as a single artifact, which hides mixed authority inside ordinary paragraphs. Compile statements can be checked against OpenAPI files, fixture payloads, and status enumerations already stored in git. Signature statements cannot be checked that way, because they create obligations the repository does not yet prove. Mixing both classes in one draft forces humans to reread tables they could have trusted and still miss a buried promise.
The practical fix is not a better prompt. It is a lane tag on every documentation block, plus a merge gate that refuses untagged or mis-tagged text. Compile lanes may be drafted by a model that only restates checked-in files. Signature lanes stay empty until a named human writes them, and the classifier treats an unsigned promise as a failed check.
Decision table: what may be drafted
Use this table as the contract for the generator, not as optional review folklore. If a block type is absent from the table, the gate must fail closed rather than guess a lane.
| Block type | Lane | Allowed inputs | Merge rule |
|---|---|---|---|
Path and operation summaries copied from OpenAPI summary
|
compile |
openapi.yaml pointer |
Fail if text diverges from the pointer |
| Parameter and field tables | compile | schema + required
|
Fail if extra columns appear |
| Request and response examples | compile | fixtures under fixtures/
|
Fail if JSON is not a fixture clone |
| Error code catalogs | compile |
components.responses or error enum |
Fail if a code lacks a schema source |
| Enum value lists | compile | schema enum
|
Fail if order or values drift |
| Auth scheme names and header identifiers | compile | securitySchemes |
Fail if a name is not in the spec |
| Deprecation or sunset wording | signature | none from the model | Fail if present without owner
|
| Support hours, channels, or severity targets | signature | none from the model | Fail if present without owner
|
| Migration deadlines and “clients must” sentences | signature | none from the model | Fail if present without owner
|
| Security guarantees and data-retention claims | signature | none from the model | Fail if present without owner
|
| Pricing, quota, and rate-limit promises | signature | none from the model | Fail if present without owner
|
The table is the artifact reviewers should argue about before any model runs. Changing a row is a docs-policy change, not a prompt tweak hidden in chat history.
Workflow
Follow the steps in order. Skipping the lane file and jumping to generation recreates the mixed-authority page this gate exists to stop.
- Inventory every heading in the reference set and map it to one row in the decision table above. Headings that do not match a row stay unpublished until the table is extended by a reviewer.
- Write a lane file that lists compile sources and signature owners, then commit that file before any draft job is allowed to start. The lane file is policy, so it needs the same review as an OpenAPI change.
- Insert a lane marker immediately under each heading, including a source pointer for compile blocks or an owner identity for signature blocks. Untagged headings are merge failures, not style nits.
- Run the compile job only against tagged compile blocks, with the model forbidden from creating new headings. New headings would bypass the inventory and smuggle signature text into a compile page.
- Run the classifier in continuous integration and reject the change when compile text matches a promise pattern or when a signature block lacks an owner. Do not rely on a later editorial pass to catch lane leaks.
- Require the signature owner to write remaining blocks in a separate commit after the compile page is already green. Mixing both edits in one diff hides which sentences were machine restated.
Lane markers in Markdown
The marker format is deliberately boring so a small script can parse it. HTML comments survive most static-site pipelines and remain invisible to readers.
## List widgets
<!-- lane:compile source:openapi.yaml#/paths/~1widgets/get -->
| Name | In | Type | Required |
| --- | --- | --- | --- |
| limit | query | integer | false |
## Support window
<!-- lane:signature owner:api-steward -->
Replacement text for this heading is written by the owner after compile checks pass.
Compile markers without source are invalid. Signature markers without owner are invalid. A heading that contains both markers is invalid, because mixed authority inside one section is the failure mode under review.
Classifier script
The following script is a proposal you can run locally. It is a gate, not a semantic proof that examples are correct, and it should fail closed on unknown headings.
#!/usr/bin/env python3
"""Fail CI when doc headings lack lanes or compile text looks like a promise."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
HEADING = re.compile(r"^(#{2,6})\s+(.+)$")
LANE = re.compile(
r"<!--\s*lane:(compile|signature)"
r"(?:\s+source:(?P<source>\S+))?"
r"(?:\s+owner:(?P<owner>\S+))?\s*-->"
)
PROMISE = re.compile(
r"(?ix)\b("
r"we\s+will|guaranteed|sla|slo|"
r"deprecated\s+on|sunset|until\s+\d{4}|"
r"clients\s+must|support\s+hours|p[1-3]\s+response|"
r"retention\s+period|rate\s+limit\s+will"
r")\b"
)
def iter_blocks(text: str):
lines = text.splitlines()
i = 0
while i < len(lines):
hm = HEADING.match(lines[i])
if not hm:
i += 1
continue
title = hm.group(2).strip()
body = []
i += 1
while i < len(lines) and not HEADING.match(lines[i]):
body.append(lines[i])
i += 1
yield title, "\n".join(body)
def check_file(path: Path) -> list[str]:
errors = []
for title, body in iter_blocks(path.read_text(encoding="utf-8")):
loc = f"{path} :: {title}"
m = LANE.search(body)
if not m:
errors.append(f"{loc}: missing lane marker")
continue
lane = m.group(1)
if lane == "compile":
if not m.group("source"):
errors.append(f"{loc}: compile lane requires source=")
if PROMISE.search(body):
errors.append(f"{loc}: compile lane contains a signature sentence")
if m.group("owner"):
errors.append(f"{loc}: compile lane must not set owner=")
elif lane == "signature":
if not m.group("owner"):
errors.append(f"{loc}: signature lane requires owner=")
if m.group("source"):
errors.append(f"{loc}: signature lane must not set source=")
return errors
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--docs", type=Path, required=True)
args = parser.parse_args()
files = sorted(args.docs.rglob("*.md"))
if not files:
print("no markdown files under", args.docs, file=sys.stderr)
return 2
errors = []
for path in files:
errors.extend(check_file(path))
for item in errors:
print(item)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
Run it against the reference tree before the compile job and again after the model writes Markdown. The second run is the one that catches leaked promises.
python3 lane_gate.py --docs ./docs/reference
echo "exit $?"
A zero exit code means every heading has exactly one valid lane. It does not mean the compile tables match the schema; that check still belongs to a schema diff or a fixture comparison in a later job.
Where a free compile job fits
A compile lane does not require a paid coding agent or a privileged production network to restate schema tables. MonkeyCode's free model access and free server option can run the restatement job against checked-in OpenAPI and fixture files. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The same lane file still blocks merge when the model emits a signature sentence, which keeps product tooling limited to the compile path.
Keep the server job dumb. Pass only the tagged compile sections and the source files named in those tags, then write output back over those sections alone. If the job is allowed to rewrite the whole page, it can delete signature markers or invent headings the inventory never approved.
Test plan for the gate
Label these cases as unexecuted until you add them to CI. They are the minimum set that proves the classifier is doing lane work rather than spelling work.
- A heading with no comment must fail, even when the body is a perfect parameter table copied from OpenAPI. Missing authority is a merge blocker.
- A compile heading whose body includes
we willor a four-digit sunset year must fail, even when a validsourcepointer is present. Promise vocabulary is not a table. - A signature heading with
owner:api-stewardand no compile source must pass, even when the body is still a placeholder sentence. Empty promises are allowed only as owned stubs. - A heading that contains both
sourceandownermust fail, because the block would again mix compile authority with signature authority. - After a model draft, rerun the gate and confirm newly added headings do not appear. Heading creation is a policy change, not a compile side effect.
Limitations
Regular expressions do not understand obligation. A compile block can still understate a required field, clone a stale fixture, or drop an error code that the schema still defines. The gate only stops untagged blocks and a small vocabulary of promise sentences; it will miss a politely worded commitment that avoids those phrases.
Signature owners can rubber-stamp a stub and leave customers without a real support window. The owner field records accountability, not completeness, and legal review remains outside this workflow. Some documentation pipelines strip HTML comments, which would remove markers and fail every heading, or worse, fail open if the strip happens after CI.
The workflow also assumes a reviewed OpenAPI file and fixture directory already exist. Without those inputs, compile lanes have nothing honest to restate, and the model will fill tables from training residue. That output is not a compile lane, even if you paste a marker above it.
Who should not use this approach
Do not apply lane tags to narrative incident reports, partner announcements, or pricing pages that are signature text from the first heading to the last. The classifier will either fail everything or teach authors to hide promises in wording the regex does not catch. Do not use it on SDKs where generated examples must compile against live clients; fixture cloning is weaker than a type-checked sample project.
Teams without a named docs steward should not enable the signature lane at all. An owner value that points at a rotating chat room creates the appearance of a signature while leaving nobody to write the block. In that case, omit the heading from the public reference set until ownership exists.
Compile-only teams that already freeze field tables from OpenAPI can add this classifier without changing release cadence. The useful first patch is the lane file and the failing test for untagged headings, not a larger generator rewrite.
Top comments (1)
The decision-table idea works because reviews of generated docs fail the same way everywhere: people read one artifact and can't see which sentences carry a promise the company has to keep. Separating restatement from obligation makes that visible before merge instead of in production.
How do you handle the leaky middle, though? Error catalogs and support-scope tables are compile-lane-looking content that quietly bind the product to the future. That boundary is where every classifier rule I've written has failed first.