DEV Community

Avery Lin
Avery Lin

Posted on

Stamp Every Doc Block With a Source SHA and an Ownership Class

Generated documentation stays honest when every block names a source SHA and an ownership class that forbids mixing. A model may fill a RESTATE block only while that SHA still matches the current git blob. A human must write every PROMISE block, including empty ones that would otherwise ship as silence. The workflow below treats Markdown sentinels as the contract, then fails CI when a block is stale, mixed, or lexically out of class.

Mixed paragraphs are the failure mode

Reviewers rarely reject a paragraph that is only half accurate against the current specification file. A generator restates a rate-limit field from OpenAPI, then appends a sentence about recommended retries. The merge still looks green, even though the recommendation has no owner and no source blob. Weeks later the spec SHA moves, the restatement is wrong, and the promise was never assigned.

The real failure is a documentation block that cannot answer two operational questions during review. Which git blob did this paragraph restate, and which role is allowed to change it later? Restatement is a function of a file at a SHA; a promise is a human speech act no blob can prove. Mixing those claims in one paragraph makes the freshness check and the review queue undefined.

Two ownership classes, never a third

Use exactly two classes in the repository, and refuse to invent a MIXED class for convenience. RESTATE covers prose that can be checked against a named source path and a full git blob SHA. PROMISE covers recommendations, SLAs, support hours, migration guarantees, and tutorial success claims. If a heading would require both classes, split the heading before any generator is allowed to run.

Decision table for draft rights

Claim type Class Model may draft Human must own CI freshness check
Field meaning copied from a schema RESTATE Yes, after a dump Review only Source SHA must match
Example payload copied from a fixture RESTATE Yes, after a dump Review only Fixture SHA must match
Error code plus message from an enum RESTATE Yes, after a dump Review only Source SHA must match
“You should retry with jitter” PROMISE No Yes Author field required
“We guarantee 99.9% monthly uptime” PROMISE No Yes Author field required
“Getting started takes five minutes” PROMISE No Yes Author field required

The table is small enough to keep beside the checker. Reviewers should reject any heading that would need both the RESTATE column and the PROMISE column at once.

Sentinel format

Keep ownership metadata in HTML comments so rendered documentation stays readable for humans. Require a closed pair, a stable id, and a hard ban on nested blocks. The SHA must be the git blob SHA of the source path, not the commit SHA of the feature branch. Blob SHAs stay stable when unrelated files move, which keeps the dirty bit precise enough for CI.

<!-- doc-block id="rate-limit-headers" class="RESTATE" source="openapi.yaml" sha="e3b0c44298fc1c149afbf4c8996fb92427ae41e4" -->

The API returns remaining quota in `X-RateLimit-Remaining`.
The header is an integer. Zero means the next request is rejected.

<!-- /doc-block -->

<!-- doc-block id="rate-limit-support" class="PROMISE" author="docs-oncall" -->

If quota issues block a production launch, email oncall@example.com.
Quota tickets are reviewed during weekday business hours in UTC.

<!-- /doc-block -->
Enter fullscreen mode Exit fullscreen mode

Compute the blob SHA with git rather than a hand-rolled hash of pretty-printed YAML. Pretty printers change whitespace, and whitespace-only churn would dirty every block without a specification change.

git hash-object openapi.yaml
git rev-parse HEAD:openapi.yaml
Enter fullscreen mode Exit fullscreen mode

Prefer git hash-object <path> inside local hooks that still allow uncommitted sources. Prefer git rev-parse HEAD:<path> in CI after the source file is already committed.

Numbered workflow

  1. Freeze the outline as block ids and classes in docs/blocks.yaml before any model run.
  2. Dump machine-readable records from schemas, fixtures, and tests into a file the generator may read.
  3. Allow a model to draft only ids whose class is RESTATE, and only inside matching sentinels.
  4. Leave PROMISE bodies empty or human-written; fail merge when a PROMISE body is still empty.
  5. Run the checker on every pull request that touches docs/ or any source path listed in sentinels.
  6. When a source SHA moves, mark the RESTATE block dirty and refuse merge until regeneration.

docs/blocks.yaml remains the human-owned map of ids, classes, and authors. A generator that invents a new id is a pipeline bug, not a helpful shortcut.

# docs/blocks.yaml
blocks:
  - id: rate-limit-headers
    class: RESTATE
    source: openapi.yaml
  - id: rate-limit-support
    class: PROMISE
    author: docs-oncall
Enter fullscreen mode Exit fullscreen mode

Artifact: an unexecuted checker and tests

The checker below is an unexecuted example for adaptation, not a report of a production measurement. It fails a file when sentinels are unclosed, when a RESTATE SHA does not match git hash-object, and when obligation language appears in a RESTATE body. It also fails when a PROMISE block lacks an author, carries source metadata, or ships with an empty body.

#!/usr/bin/env python3
"""Fail CI when doc blocks are mixed, stale, or lexically out of class."""
from __future__ import annotations

import re
import subprocess
import sys
from pathlib import Path

OPEN = re.compile(r"<!--\s*doc-block\s+(.*?)\s*-->", re.I | re.S)
CLOSE = "<!-- /doc-block -->"
ATTR = re.compile(r'(\w+)="([^"]*)"')
OBLIGATION = re.compile(
    r"\b(must|should|recommend(?:ed|s)?|always|never|"
    r"guarantee(?:s|d)?|sla|promise(?:s|d)?)\b",
    re.I,
)


def git_hash_object(path: Path) -> str:
    result = subprocess.run(
        ["git", "hash-object", str(path)],
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


def parse_attrs(blob: str) -> dict[str, str]:
    return dict(ATTR.findall(blob))


def check_file(md_path: Path) -> list[str]:
    text = md_path.read_text(encoding="utf-8")
    errors: list[str] = []
    cursor = 0
    while True:
        match = OPEN.search(text, cursor)
        if not match:
            if CLOSE in text[cursor:]:
                errors.append(f"{md_path}: unmatched closing sentinel")
            break
        attrs = parse_attrs(match.group(1))
        end = text.find(CLOSE, match.end())
        if end < 0:
            errors.append(f"{md_path}: {attrs.get('id', '?')} is unclosed")
            break
        body = text[match.end():end].strip()
        errors.extend(check_block(md_path, attrs, body))
        cursor = end + len(CLOSE)
    return errors


def check_block(md_path: Path, attrs: dict[str, str], body: str) -> list[str]:
    errors: list[str] = []
    block_id = attrs.get("id", "<missing-id>")
    klass = attrs.get("class")
    prefix = f"{md_path}#{block_id}"
    if klass not in {"RESTATE", "PROMISE"}:
        errors.append(f"{prefix}: class must be RESTATE or PROMISE")
        return errors
    if klass == "RESTATE":
        source = attrs.get("source")
        sha = attrs.get("sha", "")
        if not source:
            errors.append(f"{prefix}: RESTATE requires source=")
            return errors
        source_path = Path(source)
        if not source_path.is_file():
            errors.append(f"{prefix}: source {source} is missing")
            return errors
        actual = git_hash_object(source_path)
        if sha != actual:
            errors.append(
                f"{prefix}: stale SHA {sha[:12]}... != {actual[:12]}..."
            )
        if OBLIGATION.search(body):
            errors.append(f"{prefix}: RESTATE body contains obligation language")
    else:
        if not attrs.get("author"):
            errors.append(f"{prefix}: PROMISE requires author=")
        if not body:
            errors.append(f"{prefix}: PROMISE body is empty")
        if attrs.get("sha") or attrs.get("source"):
            errors.append(f"{prefix}: PROMISE must not carry source SHA metadata")
    return errors


def main(argv: list[str]) -> int:
    paths = [Path(a) for a in argv[1:]] or list(Path("docs").rglob("*.md"))
    errors: list[str] = []
    for path in paths:
        errors.extend(check_file(path))
    for item in errors:
        print(item, file=sys.stderr)
    return 1 if errors else 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
Enter fullscreen mode Exit fullscreen mode

A matching test plan, also unexecuted, locks the two failure classes reviewers actually miss.

# test_check_doc_blocks.py
from pathlib import Path
import check_doc_blocks as c


def test_restate_rejects_obligation(tmp_path: Path, monkeypatch):
    src = tmp_path / "openapi.yaml"
    src.write_text("openapi: 3.0.0\n")
    sha = "dummy"
    monkeypatch.setattr(c, "git_hash_object", lambda p: sha)
    md = tmp_path / "rate.md"
    md.write_text(
        f'<!-- doc-block id="h" class="RESTATE" source="{src}" sha="{sha}" -->\n'
        "You should retry.\n"
        "<!-- /doc-block -->\n"
    )
    errors = c.check_file(md)
    assert any("obligation" in e for e in errors)


def test_promise_rejects_empty(tmp_path: Path):
    md = tmp_path / "p.md"
    md.write_text(
        '<!-- doc-block id="s" class="PROMISE" author="docs-oncall" -->\n'
        "<!-- /doc-block -->\n"
    )
    errors = c.check_file(md)
    assert any("empty" in e for e in errors)
Enter fullscreen mode Exit fullscreen mode
python3 check_doc_blocks.py docs/rate-limits.md
python3 -m pytest test_check_doc_blocks.py -q
Enter fullscreen mode Exit fullscreen mode

Regenerating a dirty block

A stale SHA is a build failure, not a review comment that someone might skip. Regeneration should touch one id, rewrite the SHA attribute, and leave every PROMISE block in the same file untouched. Hand-editing a mixed sentence trains the next draft to mix again, so regeneration is the default repair.

  1. Confirm git hash-object openapi.yaml differs from the sha= attribute on the dirty block.
  2. Rebuild the machine-readable dump for that source path only, then discard the previous RESTATE draft for the id.
  3. Ask a model to restate the new dump inside the existing sentinel, with no new ids.
  4. Write the new blob SHA into sha= and run the checker before requesting review.
  5. If obligation words appear, drop the draft and regenerate instead of splicing PROMISE language into RESTATE prose.
OLD=$(python3 -c "import re,sys; print(re.search(r'sha=\"([0-9a-f]+)\"', sys.stdin.read()).group(1))" < docs/rate-limits.md)
NEW=$(git hash-object openapi.yaml)
echo "$OLD"
echo "$NEW"
# After a clean restatement:
# python3 check_doc_blocks.py docs/rate-limits.md
Enter fullscreen mode Exit fullscreen mode

Where a model belongs in this pipeline

The model is a restatement engine, not an owner of promises, SLAs, or getting-started claims. After a field dump is taken from a schema or fixture, a short prompt may ask for plain language that stays inside the RESTATE sentinel. The checker, not the prompt text, is the control that makes that constraint merge-blocking.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which can host the restatement step when another runner is not worth provisioning. Neither the model session nor the server replaces blocks.yaml, the SHA comparison, or the human author field on PROMISE blocks.

The unlabeled prompt template below is a constraint list, not a measured quality claim.

Restate only the dumped records below.
Stay inside the existing RESTATE sentinel for id=rate-limit-headers.
Do not use must, should, recommend, always, never, guarantee, or SLA.
Do not add examples that are not in the dumped records.
Do not create new block ids.
Enter fullscreen mode Exit fullscreen mode

If the model emits obligation language, discard the draft rather than teaching reviewers to salvage mixed paragraphs. Regenerating a RESTATE block is cheaper than debating a sentence that should never have been generated.

Limitations and who should not use this

The SHA match proves the source file has not moved since the restatement was stamped. It does not prove the restatement is complete, correctly scoped, or free of omitted fields. A generator can still paraphrase a type incorrectly while the SHA remains fresh against git.

The obligation lexicon will false-positive on quoted RFC fragments, discussions of HTTP, and fenced samples that contain should(). Teams that quote specification text inside RESTATE blocks need an allowlist for fenced code, not a global regex over the whole body. Quoted RFC MUST lines belong in PROMISE or in fenced excerpts, depending on whether the team is making a product commitment.

This approach is a poor fit for greenfield blogs, investor one-pagers, and pages with no schema, fixture, or test to hash. It is also a poor fit for teams that want a model to invent a tutorial narrative from a product name alone. Those pages are PROMISE pages in full, and they belong in a human queue from the first heading.

Do not treat this checker as a substitute for contract tests around example payloads. An example inside a RESTATE block should still round-trip against a recorded fixture in CI. The sentinel answers ownership and freshness only. Correctness of examples remains a test-suite problem, not a documentation-comment problem.

Merge policy

Treat a stale SHA as a failed build on protected branches, including documentation-only pull requests. Treat an empty PROMISE block as a failed build even when every RESTATE block in the file is fresh. That pairing is the operational point of the design: generated reference can move at spec speed, while promises stay visibly unfinished until a named author writes them.

Keep restatement on a disposable job, and keep PROMISE ids on a human checklist the sentinels make visible. Reviewers then read ownership in the same Markdown file that readers will see after merge.

Top comments (0)