Generated API documentation fails when extracted type facts share the same paragraph with unsigned operational promises. A sentence ledger that stamps every claim extracted, inferred, or unsigned keeps drafts useful without pretending a model measured production. Reviewers then sign only the operational sentences, and continuous integration rejects unsigned rate-limit or SLA language. The sections below specify the taxonomy, a reproducible classifier, and the remaining human-owned documentation lanes.
Mixed sentences are the defect, not missing prose
Most documentation generators still emit fluent paragraphs that hide the true origin of each clause. A required field name is extractable from a type, while a retry interval is usually an unsigned operational claim. Readers cannot see that difference once both clauses sit together inside one polished sentence. Reviewers then approve tone instead of provenance, which is how invented limits reach public pages.
This is not an argument against drafting; it is an argument against publishing mixed provenance as if it were one reviewed fact. Type-derived tables remain cheap to regenerate after every merge, provided they never absorb timeouts or quotas. Operational claims remain expensive because they imply incidents, contracts, and other customer-visible production behavior patterns. Those two cost structures should not share an unsigned sentence inside the published Markdown.
Three stamps rather than paragraph lanes
Paragraph-level compile and signature lanes still leak when one paragraph contains both a field type and a timeout. This workflow stamps every sentence as extracted, inferred, or unsigned before any publish step runs. Extracted means that every token of the claim is present in a reviewed facts file. Inferred means only grammar or grouping was added, without introducing any new operational quantities.
Unsigned means a production quantity, policy, or failure mode appeared without a human signature. Inferred wording is allowed in drafts, but continuous integration still blocks inferred sentences that contain operational keywords. Unsigned operational sentences require an explicit reviewer signature recorded beside the corresponding sentence hash. Anything else that lacks a stamp stays out of the published documentation tree entirely.
What a model may draft
A model may draft material that is reconstructable from a facts file without adding quantities. That includes parameter tables, enum member lists, and heading trees that mirror public function names. It may also rephrase extracted rows for readability, provided every quantity still matches the facts file exactly. It may not invent retry schedules, rate limits, retention windows, or any authentication failure behavior.
When a draft pass would help, MonkeyCode's free model access can propose extracted tables from the facts file. Disclosure: This article was prepared as part of MonkeyCode's product outreach. A free server option can run the same classifier in isolation beside the existing documentation job. The model may fill extracted rows; it must not mint unsigned operational sentences as if they were measured.
Operators still review inferred wording and sign every production claim before the merge completes. Free model access does not change the stamp rules, and a free server option does not replace the reviewer signature file. Treat both as optional compute for draft and check steps, not as evidence that a quantity was observed. If those options are unavailable, the same ledger still runs on any Python interpreter the docs job already uses.
What a human must own
Humans own every quantity that is not literally present in the reviewed facts file. That list includes units, the meaning of defaults, and behavior when a client omits an optional field. It also includes authentication failures, idempotency keys, pagination cursors, and example payloads that imply real production traffic. Deprecation dates, support windows, and regional residency statements stay in the unsigned lane until a named reviewer signs them.
Use the following ownership split as a review checklist, not as a substitute for incident data:
- Units, inclusive ranges, and sentinel zeros that ordinary type names cannot express on their own.
- Default semantics, including whether an omitted field inherits a server default or remains unset.
- Omission behavior for optional fields, including whether null and missing are distinct wire states.
- Authentication and authorization failure mapping onto status codes, error codes, and retry guidance.
- Rate limits, quotas, burst behavior, and backoff language that imply measured production capacity.
- Retention, deletion, backup, and residency windows that imply legal or operational policy.
- Support hours, severity definitions, and availability language that read like a contractual promise.
- Migration, deprecation, and breaking-change dates that customers would treat as a schedule.
Decision table for each generated sentence
| Sentence pattern | Stamp | May publish |
|---|---|---|
| Name, type, or required flag copied from the facts file | extracted |
Yes, after the sentence hash matches the facts row |
| Grammar-only rewrite of an extracted row, no new quantity | inferred |
Yes, unless an operational keyword is present |
| Timeout, quota, SLA, retention, or auth-failure quantity | unsigned |
Only with a reviewer signature on that hash |
| Example payload that implies live traffic or customer data | unsigned |
Only with a reviewer signature on that hash |
| Heading that only restates a public function identifier | extracted |
Yes, after the identifier exists in the facts file |
The table is the contract. Extracted rows are cheap because a compiler can rebuild them after every type change. Inferred rows are cheap only while they remain quantity-free restatements of those extracted rows. Unsigned rows are expensive because a person is asserting production behavior, not restating a type. Continuous integration should encode that cost difference instead of trusting paragraph tone.
Step 1. Compile a facts file from types, not from chat
Public function signatures, dataclass fields, and JSON Schema required arrays remain valid extract sources. Chat transcripts are not valid extract sources, even when they sound confident about the same API. The compiler writes a JSON array of claims with stable ids, source paths, and normalized values. Reviewers treat that file as the only input the model is allowed to quote.
# extract_facts.py — example compiler (unexecuted here; run against your tree)
import ast, json, pathlib, hashlib
def fields_from(path: pathlib.Path) -> list[dict]:
tree = ast.parse(path.read_text(encoding="utf-8"))
rows = []
for node in ast.walk(tree):
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
typ = ast.unparse(node.annotation)
name = node.target.id
claim = f"{path.name}:{name}:{typ}"
rows.append({
"id": hashlib.sha256(claim.encode()).hexdigest()[:16],
"path": str(path),
"name": name,
"type": typ,
"required": node.value is None,
})
return rows
The compiler above is intentionally narrow: it records names, annotations, and a coarse required flag. It does not infer milliseconds, retries, or status codes from comments, because comments are not types. If a comment contains an operational quantity, move that quantity into a signed claims file instead of hoping the model will ignore it. Keep the facts file in version control beside the source it describes.
Step 2. Draft only extracted tables from those rows
Feed the facts file to a draft step that may emit Markdown tables and heading trees. Prohibit the draft prompt from adding numbers that are absent in the facts file, including example status codes. If the draft needs connective English, mark those sentences inferred in the next step rather than pretending they were extracted. Discard any draft sentence that introduces a unit, duration, quota, or failure mode.
# example prompt constraints (proposal, not a measured template)
- Quote only ids, names, types, and required flags from facts.json.
- Do not invent timeouts, rate limits, retries, SLAs, or status mappings.
- Do not write example request bodies that resemble production traffic.
- Leave a placeholder token UNSIGNED_CLAIM wherever a human must supply a quantity.
Placeholder tokens are more honest than fluent guesses. A visible UNSIGNED_CLAIM token forces a reviewer to replace it with a signed sentence or delete the clause. Fluent guesses hide the missing measurement inside grammar that looks finished. Documentation review should prefer an ugly token over a confident vacuum.
Step 3. Split the draft into sentences and stamp each one
Sentence splitting does not need a language model. A conservative splitter plus the facts file is enough to label extracted rows. Operational keyword matching then promotes remaining sentences to unsigned when they mention capacity, time, policy, or failure. Everything left is inferred and still cannot carry those keywords into the published tree.
# stamp_docs.py — example classifier (unexecuted here)
import hashlib, json, re, pathlib
SENTENCE = re.compile(r"(?<=[.!?])\s+")
OPERATIONAL = re.compile(
r"\b(rate limit|rps|qps|sla|p99|timeout|retry|retention|uptime|backoff)\b",
re.I,
)
def stamp(markdown: str, facts: list[dict]) -> list[dict]:
fact_tokens = set()
for row in facts:
fact_tokens.update({row["name"].lower(), row["type"].lower()})
out = []
for raw in SENTENCE.split(markdown.strip()):
sentence = " ".join(raw.split())
if not sentence:
continue
digest = hashlib.sha256(sentence.encode()).hexdigest()[:16]
words = set(re.findall(r"[a-z0-9_]+", sentence.lower()))
extracted = bool(words & fact_tokens) and not OPERATIONAL.search(sentence)
kind = "extracted" if extracted else (
"unsigned" if OPERATIONAL.search(sentence) else "inferred"
)
if kind == "inferred" and OPERATIONAL.search(sentence):
kind = "unsigned"
out.append({"hash": digest, "stamp": kind, "text": sentence})
return out
The classifier is deliberately conservative and will over-promote borderline sentences into unsigned. That is the correct failure direction for public API documentation. Under-promotion would let a timeout hide inside an inferred rewrite of a type table. Over-promotion only creates extra review work, which is cheaper than a fabricated quota.
Step 4. Sign unsigned hashes in a separate ledger
Do not sign the Markdown file as a blob. Sign each unsigned sentence hash so a later wording tweak cannot reuse an old approval. The signature file records reviewer identity, date, and the exact sentence text that was approved. Regenerated drafts may keep an old signature only when the hash still matches.
{
"signatures": [
{
"hash": "a1b2c3d4e5f67890",
"reviewer": "docs-oncall",
"date": "2026-09-22",
"text": "Authenticated clients may send 100 requests per minute per API key."
}
]
}
The example quantity above is illustrative, not a measured limit from any product described here. Replace it with a number your operator actually owns, or delete the sentence. A signature without an owner name is not a signature for this workflow. Dates belong on signatures because operational claims expire when capacity and policy change.
Step 5. Fail CI when operational language lacks a matching signature
The check belongs in the documentation job, not in a manual preview comment. Load the stamped sentences, load the signature ledger, and fail on any unsigned operational hash. Also fail inferred sentences that still match the operational keyword list, because inference is not a path around review. Keep the test boring so it can run on every docs-changing pull request.
# test_docs_stamps.py — example check (unexecuted here)
import json, pathlib, pytest
from stamp_docs import stamp
def test_no_unsigned_operational_sentences():
facts = json.loads(pathlib.Path("facts.json").read_text())
markdown = pathlib.Path("api.md").read_text()
signed = {
row["hash"] for row in json.loads(
pathlib.Path("signatures.json").read_text()
)["signatures"]
}
failures = []
for row in stamp(markdown, facts):
if row["stamp"] == "unsigned" and row["hash"] not in signed:
failures.append(row)
if row["stamp"] == "inferred" and row["hash"] not in signed:
# inferred rows should not need signatures unless keywords slipped through
pass
assert failures == [], failures
Extend the assertion if your corpus uses HTML tables or admonition blocks instead of plain sentences. The important property is not the splitter; it is that unsigned operational language cannot reach the default branch. If the test is noisy, shrink the operational keyword list with review, rather than weakening the unsigned rule. Keyword lists are policy, and policy belongs in code review like any other contract.
Limitations
This workflow does not measure production, and it does not certify that a signed number is still true next quarter. Sentence splitting is brittle on abbreviations, version numbers, and tables that contain multiple claims per cell. Keyword lists both over-match ("timeout" in a field name) and under-match (a novel capacity noun the regex never saw). Extracted stamps are only as correct as the compiler, which ignores comments, side effects, and runtime feature flags.
The method also assumes a facts file exists and is reviewed independently of the prose. Teams that generate docs solely from chat output cannot stamp extracted rows, because nothing was extracted. Teams that need legal privacy statements, security advisories, or customer contracts need counsel, not a regex. A free draft pass does not reduce those limitations, and a free check runner does not make an unsigned SLA safe to publish.
Who should not use this approach
Skip this ledger if the document is an internal brainstorm and nobody will treat it as customer-facing truth. Skip it if your API surface is a single README paragraph that a human already writes without a model. Skip it if you need the model to author incident retrospectives, pricing pages, or availability promises from incomplete telemetry. Skip it if you cannot keep a facts file in version control beside the code being described.
Do not use the stamps as a substitute for load tests, audit logs, or an on-call rotation. A green documentation job means unsigned operational language was blocked or signed, not that the signed language is currently accurate. Re-sign capacity claims when the underlying limit changes, even if the Markdown hash would otherwise stay valid after a trivial rewrite. The ledger tracks provenance; it does not track production.
Closing
Publish type-derived documentation as extracted tables, and keep inferred grammar quantity-free. Route every timeout, quota, failure mode, and example that implies live traffic through unsigned hashes with named reviewers. If a sentence cannot be stamped, it does not belong on the default branch beside the API. That split is the entire method; the classifier is only a way to keep it honest under merge pressure.
Top comments (0)