DEV Community

Avery Lin
Avery Lin

Posted on

Stamp Every Docs Unit Inventory, Procedure, or Promise Before Drafting

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

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

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

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

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

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

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

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

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

  1. Every Markdown unit under docs/ has lane and sources in front matter.
  2. Inventory units cite a spec pointer or a fixture path at a recorded SHA.
  3. Procedure units that a model drafted still show the proposal banner.
  4. Promise units set author: human and cite a policy file or tracker record.
  5. lane_lint.py and test_lane_lint.py run 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)