Generated API docs fail review when inventory, procedure, and promise language occupy the same paragraph without an owner. A classifier stamps each documentation unit with one lane, and a CI linter rejects mixed commitments so model drafts stay inside restatable facts. Inventory units restate schemas, recorded HTTP fixtures, and route tables that already exist in the repository. Procedure units restate runbooks as labeled proposals, while promise language stays human-authored because it creates unprovable obligations.
The failure mode this gate targets
Reviewers rarely block generated reference pages because a field name is misspelled in a table. They block the page because the same block also promises retries, retention windows, or customer support. A model that restates GET /v1/orders/{id} from OpenAPI can list path parameters without inventing runtime behavior. When that paragraph says the endpoint will remain available, the sentence leaves evidence the repository can actually prove.
Mixing those speech acts inside one unit makes the merge decision binary for the whole page. Useful inventory then waits behind unowned promises, which is a process failure rather than a model-quality failure. This workflow treats a documentation unit as one Markdown file, or one fenced section, that carries YAML front matter. Each unit receives exactly one lane stamp before any model is allowed to draft replacement prose.
Continuous integration then scans the drafted text for lane violations using a small, tested classifier instead of a qualitative style guide. The classifier is conservative on purpose: it does not score tone, completeness, or readability. It only answers whether a unit stayed inside the lane that reviewers already agreed to automate. Broader doc quality remains a human merge judgment after the lane gate is green.
Lane definitions and a decision table
Use three lanes only, because extra categories collapse during review and recreate the mixed-paragraph problem. Inventory restates machine-readable structure. Procedure restates operator steps that might be wrong even when the schema is correct. Promise language creates external expectations that tests and OpenAPI files cannot confirm.
| Lane | Evidence the unit may cite | Model may draft? | Human must own |
|---|---|---|---|
| Inventory | OpenAPI path item, JSON Schema, recorded HTTP fixture, generated field list | Yes, as restatement of identifiers already extracted | Accuracy of those source files |
| Procedure | Checked-in runbook, playbook, or operator script with a stable path | Only as a labeled proposal | Whether the steps are the supported method |
| Promise | Signed policy file, legal record, or tracker issue that a human cites | No | SLAs, deprecation dates, security guarantees, support hours, retention |
Promise language includes availability, durability, legal processing, customer support, and any calendar date that creates an external expectation. Procedure language includes ordered operator steps whose correctness depends on production access, not on schema validation. Inventory language names types, required flags, status codes observed in fixtures, and error identifiers listed in the spec snapshot.
Numbered workflow
1. Split the docs tree into stamped units
Create docs/inventory, docs/procedure, and docs/promise so the path prefix and the front matter cannot disagree. Each file starts with a lane key and a sources list, and the generator must refuse files that omit either field. Reviewers then know the unit type before they read the first sentence of prose.
---
lane: inventory
sources:
- openapi/openapi.yaml#/paths/~1v1~1orders~1{id}/get
- tests/fixtures/http/get_order_200.json
evidence_sha: 4b2c0e1a
---
Procedure files set lane: procedure and proposal: true when a model produced the first draft. Promise files set lane: promise and author: human, which the linter treats as a hard requirement. A generator that writes outside these three directories is out of scope for this gate and should fail a separate path check.
2. Freeze the evidence before any prose draft
Export a spec snapshot and the fixture set at a git tree SHA so the draft cannot chase a moving working copy. Record that SHA in the unit front matter as evidence_sha and keep the extractor output in version control. Inventory prose that cites a path missing from the snapshot is a classifier failure even when the live branch already added the path.
mkdir -p docs/inventory docs/procedure docs/promise
git rev-parse HEAD:openapi/openapi.yaml
git rev-parse HEAD:tests/fixtures/http
python tools/extract_inventory.py \
--spec openapi/openapi.yaml \
--fixtures tests/fixtures/http \
--out docs/inventory/_index.json
The extractor should emit identifiers only: operationId, method, path, status codes present in fixtures, and schema pointers. It should not emit sentences, modal verbs, or operator advice. Label any richer summary as an unexecuted sketch until the linter and tests exist in the same change.
3. Draft inventory only from the extracted index
A model may turn _index.json into Markdown tables and short restatements that reuse those identifiers. It may not add obligation verbs, calendar sunsets, or support claims, even when those claims are probably true. Keep the constraint file in the repository beside the classifier so reviewers can see what the draft step was allowed to do.
Restate each operation in docs/inventory/_index.json.
Use only identifiers present in that file.
Do not write guarantee, SLA, retain, support hours, or sunset dates.
Do not invent status codes, headers, or authentication behavior.
A free-model draft step can rewrite Lane A inventory from frozen fixtures when you want a first prose pass. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that isolated rewrite job, provided the linter still blocks Lane C language before merge.
4. Keep procedure drafts labeled and non-normative
If a runbook exists at ops/runbooks/replay-outbox.md, a model may summarize it into docs/procedure/replay-outbox.md with an explicit proposal banner. The banner must remain until a human removes proposal: true after executing the steps against a real environment. Untested procedure prose is still more dangerous than inventory because operators may follow it during an incident.
---
lane: procedure
proposal: true
sources:
- ops/runbooks/replay-outbox.md
---
> Proposal only. This summary restates `ops/runbooks/replay-outbox.md` and is not an approved procedure until `proposal` is false.
Do not let procedure files cite only OpenAPI pointers, because a path item does not describe operator ordering, lock files, or rollback. If no runbook exists, skip generation entirely and leave a human-owned stub that states the gap. A missing procedure is cheaper than a fluent procedure that nobody has rehearsed.
5. Write promise units by hand and bind them to a policy path
Deprecation dates, advertised token lifetimes, and retention windows belong under docs/promise. Each promise unit cites a policy file or an issue tracker record, not an OpenAPI description block. If no policy file exists, the unit must not merge, because the classifier cannot verify the claim against repository evidence.
---
lane: promise
author: human
sources:
- policies/retention.md
- issues/1428
---
Security guarantees, compliance labels, and support hours follow the same rule even when a model could phrase them fluently. Fluency is not evidence. The human author field is the merge signal that someone accepted the obligation.
6. Lint mixed language in CI
Run the classifier on every Markdown unit under docs/ in the same job that builds the site. Fail the build when inventory files contain promise patterns, when procedure files lack the proposal banner or a runbook source, or when promise files were authored as generator. Keep the command short so the gate is obvious in CI logs.
python tools/lane_lint.py docs
pytest tools/test_lane_lint.py
Artifact: a reproducible lane linter
The following module is a starting classifier, not a linguistic theory of technical writing. It is deliberately conservative: inventory files fail on a small set of obligation verbs, calendar promises, and compliance labels. Extend the pattern list only when a new test shows a mixed-lane sentence that reviewers already rejected.
# tools/lane_lint.py
from __future__ import annotations
import re
import sys
from pathlib import Path
import yaml
PROMISE_PATTERNS = [
re.compile(r"\b(guarantee|SLA|uptime|retain(?:ed|s)?|never lose)\b", re.I),
re.compile(r"\b(we will|must always|24/7|support hours)\b", re.I),
re.compile(r"\b(deprecated on|sunset on|available until)\b", re.I),
re.compile(r"\b(end-to-end encrypted|HIPAA|SOC 2)\b", re.I),
]
def in_root(path: Path, root: str) -> bool:
norm = path.as_posix().replace("\\", "/")
return (
norm == root
or norm.startswith(root + "/")
or f"/{root}/" in f"/{norm}/"
)
def split_front_matter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
raise ValueError("missing YAML front matter")
parts = text.split("---", 2)
if len(parts) < 3:
raise ValueError("unterminated YAML front matter")
return yaml.safe_load(parts[1]) or {}, parts[2]
def classify_violation(path: Path, meta: dict, body: str) -> str | None:
lane = meta.get("lane")
sources = meta.get("sources") or []
if lane == "inventory":
if not in_root(path, "docs/inventory"):
return f"{path}: inventory lane outside docs/inventory"
if not sources:
return f"{path}: inventory unit missing sources"
for pat in PROMISE_PATTERNS:
if pat.search(body):
return f"{path}: inventory unit contains promise language: {pat.pattern}"
return None
if lane == "procedure":
if not in_root(path, "docs/procedure"):
return f"{path}: procedure lane outside docs/procedure"
if not sources:
return f"{path}: procedure unit missing runbook sources"
if meta.get("proposal") is True and "Proposal only" not in body:
return f"{path}: proposal procedure missing banner"
return None
if lane == "promise":
if not in_root(path, "docs/promise"):
return f"{path}: promise lane outside docs/promise"
if meta.get("author") != "human":
return f"{path}: promise unit must set author: human"
if not sources:
return f"{path}: promise unit missing policy sources"
return None
return f"{path}: unknown or missing lane"
def lint_tree(root: Path) -> list[str]:
errors: list[str] = []
for path in sorted(root.rglob("*.md")):
meta, body = split_front_matter(path.read_text(encoding="utf-8"))
err = classify_violation(path, meta, body)
if err:
errors.append(err)
return errors
def main() -> int:
root = Path(sys.argv[1] if len(sys.argv) > 1 else "docs")
errors = lint_tree(root)
for err in errors:
print(err)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
Pair the module with tests that encode the decision table rather than reviewer taste. The cases below are the intended contract for the linter; they are not reported production metrics. Add a fixture timestamp inside a fenced JSON block if you need a date that must not trigger promise patterns.
# tools/test_lane_lint.py
from pathlib import Path
from lane_lint import classify_violation, split_front_matter
def test_inventory_rejects_sla_sentence() -> None:
text = """---
lane: inventory
sources:
- openapi/openapi.yaml#/paths/~1health/get
---
The health endpoint will remain available and includes an SLA.
"""
path = Path("docs/inventory/health.md")
meta, body = split_front_matter(text)
err = classify_violation(path, meta, body)
assert err is not None
assert "promise language" in err
def test_inventory_allows_status_from_fixture() -> None:
text = """---
lane: inventory
sources:
- tests/fixtures/http/get_health_200.json
---
`GET /health` returns `200` with `{ "status": "ok" }` in the recorded fixture.
"""
path = Path("docs/inventory/health.md")
meta, body = split_front_matter(text)
assert classify_violation(path, meta, body) is None
def test_procedure_proposal_requires_banner() -> None:
text = """---
lane: procedure
proposal: true
sources:
- ops/runbooks/replay-outbox.md
---
Replay the outbox table, then confirm the cursor advanced.
"""
path = Path("docs/procedure/replay-outbox.md")
meta, body = split_front_matter(text)
err = classify_violation(path, meta, body)
assert err is not None
assert "proposal procedure missing banner" in err
def test_promise_requires_human_author() -> None:
text = """---
lane: promise
author: generator
sources:
- policies/retention.md
---
Order payloads are retained for thirty days.
"""
path = Path("docs/promise/retention.md")
meta, body = split_front_matter(text)
err = classify_violation(path, meta, body)
assert err is not None
assert "author: human" in err
Limitations and who should not use this
The classifier does not prove that inventory restatements are true; it only proves they do not contain a small obligation lexicon. Semantic drift still needs schema diffs and fixture hashes, which are complementary gates rather than replacements. Regex will over-trigger on words inside quotations and under-trigger on polite commitments that avoid the listed verbs. Treat every new pattern as a tested change, not as an editorial impulse during incident review.
Do not use this approach when the product has no OpenAPI document, no recorded fixtures, and no runbooks. Marketing pages, partner pitches, and terms of service are promise documents by nature and should not pass through a model draft step at all. Teams generating a single narrative README for a private prototype will find the directory split heavier than the review pain it removes.
Calendar dates in inventory examples, such as a fixture timestamp, should live inside fenced JSON so the promise patterns do not fire on example payloads. If a specification RFC requires the word "must" in customer-facing text, move that sentence into a promise or specification unit that cites the RFC instead of loosening the inventory lexicon. The lane stamp is a merge constraint, not a writing style guide, and it will not replace legal review for promise files.
Review checklist
- Every Markdown unit under
docs/haslaneandsourcesin front matter. - Inventory units cite a spec pointer or a fixture path at a recorded SHA.
- Procedure units that a model drafted still show the proposal banner.
- Promise units set
author: humanand cite a policy file or tracker record. -
lane_lint.pyandtest_lane_lint.pyrun in the same CI job that builds the docs site.
Once inventory, procedure, and promise cannot share a paragraph, model drafts become cheaper to accept because their failure mode is bounded. If you already fail CI on broken OpenAPI examples, adding lane_lint.py beside that job keeps promise authorship in review without a new publishing stack.
Top comments (0)