Generated API reference can be compiled from a reviewed specification, but guarantee language cannot travel with it. A documentation build should fail when a model-written page contains modal claims that no human has signed in a claim ledger. The rest of this article gives a scanner, a ledger schema, and a decision table that keep those two lanes from mixing during generation.
This workflow is not a style guide for writers, and it is not a substitute for legal or security review. It treats unsigned promises as build errors, the same way an unsigned container image is treated as a deploy error. Teams that already compile parameter tables from OpenAPI still leak guarantee sentences when a drafting model improvises customer-facing claims. The gate below exists to make that improvisation visible before the documentation site is allowed to publish.
What the model may draft versus what a human must own
Compile-lane text is any sentence whose truth already lives in a reviewed artifact checked into the repository. That artifact is usually a pinned OpenAPI document, a fixture set, or a facts file that a human already accepted. Signature-lane text is any sentence that creates an operational, legal, or customer-facing promise the specification does not encode. Mixing those lanes in one generated file is how retention claims, sunset dates, and support hours appear without an owner.
The table below is the contract for this workflow and should be copied into the docs repository beside the ledger. If a row is marked compile, a model may draft that fragment from the reviewed source and nothing else. If a row is marked sign, the sentence must exist in the ledger before the documentation build is allowed to pass.
| Doc fragment | Source of truth | Lane | Model may draft? |
|---|---|---|---|
| Path, method, and status-code lists | OpenAPI paths
|
compile | yes, from the pinned spec only |
| Parameter names, types, and required flags | OpenAPI parameters
|
compile | yes, from the pinned spec only |
| Example bodies bound to reviewed fixtures | checked-in cassettes | compile | yes, copy only |
| Field descriptions copied verbatim | spec description
|
compile | yes, if the text is unchanged |
| Rate-limit numbers and burst behavior | signed claim ledger | sign | no |
| Retention, deletion, and PII language | signed claim ledger | sign | no |
| Support hours and escalation paths | signed claim ledger | sign | no |
| Deprecation calendars and sunset dates | signed claim ledger | sign | no |
| Availability, SLA, and never/always claims | signed claim ledger | sign | no |
| Pricing, credits, and refund language | signed claim ledger | sign | no |
A drafting model that is useful on compile-lane tables remains the wrong author for signature-lane sentences. Cheap regeneration is acceptable for the first column because the output can be diffed against the specification. Guarantee sentences need a named owner, a signature date, and a hash of the normalized text, not another regenerated paragraph.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Compile-lane drafting does not need dedicated inference hardware when every table is checked against a pinned specification file. MonkeyCode's free model access and free server option can host that regeneration step without expanding the trust boundary of the ledger. The claim gate still runs in your own CI, because the ledger is human-owned and must stay unwritable to the drafter.
Keep promises in a ledger, not in generated pages
Keep the ledger beside the docs tree, and never inside the folder the compiler is allowed to rewrite. Generated pages are disposable outputs of a pinned spec; the ledger is a human-owned source file. Each entry records one promise sentence, a stable identifier, an owner, and a signature date that reviewers can grep during incident response.
# docs/claims/ledger.yaml
version: 1
claims:
- id: logs-retention-90d
sentence: Request logs are retained for 90 days.
owner: docs-oncall
signed: "2026-09-14"
- id: no-training-on-payloads
sentence: Request payloads are not used to train models.
owner: security-docs
signed: "2026-09-14"
- id: sunset-notice-90d
sentence: Deprecated endpoints remain reachable for 90 days after the notice.
owner: api-governance
signed: "2026-09-14"
Normalization is required, or trivial punctuation edits will look like brand-new promises during the hash comparison. Lowercase the sentence, collapse internal whitespace, and strip trailing punctuation before hashing so reviewers compare meaning rather than wrapping. Hash only the normalized sentence string, not the YAML wrapper, so a later owner rename does not silently invalidate a claim that nobody actually changed.
Protect the ledger with the same controls you already use for deploy credentials metadata, minus the secrets themselves. A CODEOWNERS rule plus a write freeze during generation is enough for most teams that already review documentation pull requests.
# .github/CODEOWNERS
/docs/claims/ @api-governance @security-docs
chmod a-w docs/claims/ledger.yaml
# drafter process should receive only docs/generated as a writable output root
Numbered workflow
Follow the steps in order, because skipping the freeze on the ledger folder is how unsigned promises re-enter the site. Each step is a separate process so a compiler crash cannot leave a half-written guarantee sitting in a generated markdown file.
- Freeze human-owned paths, including
docs/claims/and policy markdown, as unwritable to the drafter process before any generation starts. - Compile reference pages from the pinned OpenAPI file or reviewed facts file into
docs/generated/, using whatever compiler you already trust for tables. - Run the claim scanner on
docs/generated/and on any mixed markdown the template still emits into that output directory. - Fail the build when a promise-shaped sentence is missing from the ledger or when its normalized hash does not match a signed row.
- If the product actually changed a promise, add a ledger entry in a separate human commit, then regenerate the compile-lane pages.
The scanner below is a labeled example for CI, not a benchmark of any model and not a measurement of production traffic. Copy it into the docs job as a deterministic check that does not call a network model at review time.
#!/usr/bin/env python3
"""claim_gate.py — fail if generated docs contain unsigned promise sentences."""
from __future__ import annotations
import argparse
import hashlib
import re
import sys
from pathlib import Path
import yaml
PROMISE_RE = re.compile(
r"\b("
r"will|must|never|always|guarantee(?:s|d)?|sla|"
r"retain(?:ed|s)?|retention|pii|gdpr|hipaa|"
r"uptime|available 24|support hours|"
r"deprecat(?:e|ed|ion)|sunset|"
r"encrypt(?:ed|ion)|not used to train"
r")\b",
re.IGNORECASE,
)
SENTENCE_RE = re.compile(r"(?<=[.!?])\s+")
def normalize(sentence: str) -> str:
text = " ".join(sentence.strip().split()).lower()
return text.rstrip(".;:!")
def sentence_hash(sentence: str) -> str:
return hashlib.sha256(normalize(sentence).encode("utf-8")).hexdigest()
def load_ledger(path: Path) -> dict[str, str]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
mapping = {}
for row in data.get("claims", []):
mapping[sentence_hash(row["sentence"])] = row["id"]
return mapping
def iter_markdown(root: Path):
for path in sorted(root.rglob("*.md")):
yield path, path.read_text(encoding="utf-8")
def extract_sentences(text: str):
chunks = SENTENCE_RE.split(text.replace("\n", " "))
for chunk in chunks:
sentence = chunk.strip()
if sentence:
yield sentence
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--generated-dir", type=Path, required=True)
parser.add_argument("--ledger", type=Path, required=True)
args = parser.parse_args(argv)
ledger = load_ledger(args.ledger)
failures: list[str] = []
for path, text in iter_markdown(args.generated_dir):
for sentence in extract_sentences(text):
if not PROMISE_RE.search(sentence):
continue
digest = sentence_hash(sentence)
if digest not in ledger:
failures.append(f"{path}: unsigned claim: {sentence}")
for line in failures:
print(line, file=sys.stderr)
print(f"unsigned_claim_count={len(failures)}")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
A matching test plan keeps the gate honest when someone later “improves” the regex. Save the cases as fixtures in the docs CI job rather than relying on a chat transcript that nobody can replay.
# tests/claim_gate.plan.md
1. Empty generated dir + empty ledger => exit 0, unsigned_claim_count=0
2. Generated sentence "The limit parameter is an integer." => exit 0
3. Generated sentence "Logs are retained for 90 days." without ledger => exit 1
4. Same sentence present in ledger.yaml => exit 0
5. Ledger sentence with extra spaces or a trailing period => still exit 0
6. Drafter cannot write docs/claims/ledger.yaml (mode bits or CI path filter)
Wire generation and gating as two commands so a compiler that prints a guarantee cannot hide inside the same process that is supposed to reject it.
python3 compile_reference.py --spec openapi.yaml --out docs/generated
python3 claim_gate.py --generated-dir docs/generated --ledger docs/claims/ledger.yaml
How to read scanner output
Treat unsigned_claim_count as a release blocker, not as a writing-quality score or an engagement metric. A count of zero means no promise-shaped sentence appeared in generated markdown outside the signed ledger. A count greater than zero means a human must either delete the sentence from the compile template or promote it into the ledger with an owner and a date.
Do not “fix” a failing claim by asking a model to rephrase until the regular expression goes quiet. Rephrasing “we always encrypt payloads” into “encryption is typically applied” hides the product question instead of answering it for the next reviewer. If the product does encrypt in that way, add a signed sentence with an owner who can defend it. If the product does not make that promise, remove the sentence from the compile-lane template and leave the ledger unchanged.
When a hash mismatch appears against an existing identifier, read it as an edit to a promise rather than as a formatter nit. Changing “90 days” to “30 days” is a product change, and it belongs in a human commit that updates the ledger before any generated page is allowed to mention the new window. The scanner should fail closed on that mismatch so a regenerate-and-hope loop cannot silently shorten a retention claim.
Limitations and who should not use this
Sentence splitting on periods fails inside version numbers, IPv4 addresses, and abbreviations such as U.S. or e.g. in running text. The regular expression will miss cleverly worded promises, and it will also flag innocent method names that contain must or deprecated in code spans if you do not strip fences first. It is a tripwire for accidental generation, not a proof that the documentation is legally complete.
Do not use this workflow as the only control on medical, financial, or government documentation, where counsel and a named reviewer still own the pages. Do not use it on marketing sites where metaphor and superlatives are the product voice rather than an accident. Do not point a drafter at a live OpenAPI URL; compile from a reviewed, pinned spec file so tables cannot drift while the job is running.
Teams without a reviewed specification should not generate reference documentation at all under this method. The compile lane has nothing trustworthy to compile, and a model will fill the gap with invented endpoints, status codes, and parameter types. Teams that currently paste tokens, session cookies, or customer payloads into example blocks should keep those blocks out of generation entirely, which is a separate control from the claim ledger and is not solved by hashing sentences.
The ledger also does not version promises across product lines that share one docs tree. If two APIs publish from the same repository, give each claim an identifier that includes the product slug, or the scanner will accept a sentence signed for the wrong audience. Shared trees need per-product generated directories as well, so a compile job for one API cannot rewrite tables that another API’s reviewers already signed off.
Closing
Compile the tables, examples, and inventories that a pinned specification already proved, and keep that output disposable. Sign the sentences that tell a customer what the product will do with their data, their uptime, and their deprecation calendar. Refresh the first set whenever the reviewed spec changes; move the second set only when a human updates the ledger and the build can hash the new promise.
Top comments (0)