DEV Community

Morgan Sun
Morgan Sun

Posted on

Draft Ceilings: Let the Model Write, but Make Git Prove Who Owned the Sentence

You have probably reviewed a pull request like this one: 40 documentation files changed, one model pass, tidy diffs. The review takes nine minutes because prose quality is easy to skim.

Three weeks later someone quotes a support matrix sentence in a customer thread. The sentence is wrong. Nobody can say who owned it, because the diff looked fine and the model did not sign anything.

The useful question is not "was this AI-written?" It is: which sentences did a human last own, and which ones only ever passed through a model? This article builds a gate for that. It takes about 40 lines of git plumbing and one commit convention.

What a model may draft, and what a human must own

Split every doc section into two classes before you generate anything.

Model-may-draft (prose, not commitments):

  • Overview paragraphs and "why this exists" framing
  • Glossary entries derived from code identifiers
  • Cross-links and table-of-contents order
  • Changelog summaries assembled from merged PR titles
  • Tutorial narration around commands that already exist in the repo

Human-must-own-last (a wrong sentence here costs money):

  • Support / compatibility matrices
  • Limits, quotas, pricing, SLA wording
  • Deprecation notices and removal dates
  • Security guidance and anything that says "never" or "must"
  • Migration step order (a wrong order breaks data)
  • Error semantics and retry behavior

The distinction that matters is commitment density, not length. A 600-word friendly intro probably commits to nothing. A one-line table cell can commit the company.

Artifact: a draft ceiling computed from git blame

Instead of linting documents, audit their provenance. Two pieces: a commit trailer, and a checker that runs per section.

1. The convention: every docs commit declares its lane

Regenerate API overview

Doc-Draft: model
Source: src/platform.ts
Enter fullscreen mode Exit fullscreen mode

Rules that keep this honest:

  1. Doc-Draft: model if any line in the diff was generated and not edited by a human.
  2. Doc-Draft: human if a person wrote or rewrote the changed lines.
  3. A human editing a model draft replaces the trailer, not appends to it.
  4. Source: names the file, issue, or URL the claim came from.

2. The checker: per-section model share, gated on commitments

Save as tools/doc_ceiling.py (Python 3.9+, no dependencies):

#!/usr/bin/env python3
"""doc_ceiling.py -- flag doc sections a model draft owns last.

Exit 1 if a section containing commitment language has more than
`ceiling` of its lines last touched by a `Doc-Draft: model` commit.
"""
import re
import subprocess
import sys

COMMITMENT = re.compile(
    r"\b(must|never|guarantee[sd]?|deprecat\w+|supported|unsupported|"
    r"breaking|limit(?:ed)?|price[sd]?|cost[s]?|SLA|security)\b",
    re.IGNORECASE,
)
HEADING = re.compile(r"^##\s+(.*)$")
SHA_LINE = re.compile(r"^([0-9a-f]{40}) \d+ \d+")


def run(*args):
    return subprocess.run(args, capture_output=True, text=True, check=True).stdout


def trailer(sha, key):
    for line in run("git", "log", "-1", "--format=%B", sha).splitlines():
        m = re.match(rf"^{key}:\s*(\S+)\s*$", line)
        if m:
            return m.group(1)
    return None


def audit(path, ceiling):
    text = run("git", "show", f"HEAD:{path}").splitlines()
    blame = run("git", "blame", "--line-porcelain", "--", path).splitlines()
    shas = [m.group(1) for m in map(SHA_LINE.match, blame) if m]
    if len(shas) != len(text):
        sys.exit(f"{path}: blame/text mismatch ({len(shas)} vs {len(text)})")

    kinds, sections, cur = {}, [], {"title": "(preamble)", "rows": []}
    for body_line, sha in zip(text, shas):
        if HEADING.match(body_line):
            sections.append(cur)
            cur = {"title": HEADING.match(body_line).group(1).strip(), "rows": []}
        if sha not in kinds:
            kinds[sha] = trailer(sha, "Doc-Draft") or "human"
        cur["rows"].append((body_line, kinds[sha]))
    sections.append(cur)

    failed = False
    for sec in sections:
        total = len(sec["rows"])
        model = sum(1 for _, k in sec["rows"] if k == "model")
        commits = any(COMMITMENT.search(l) for l, _ in sec["rows"])
        share = model / total if total else 0.0
        flag = commits and share > ceiling
        failed |= flag
        print(f"{'FAIL' if flag else 'ok  '} {share:5.2f} "
              f"{'commitment' if commits else 'narrative ':10s} {sec['title']}")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(audit(sys.argv[1], float(sys.argv[2]) if len(sys.argv) > 2 else 0.34))
Enter fullscreen mode Exit fullscreen mode

Run it on the file you just regenerated:

python3 tools/doc_ceiling.py docs/api.md 0.34
Enter fullscreen mode Exit fullscreen mode

3. Verify the checker before you trust it

Do not wire a gate you have not seen fail. Four commands, disposable repo:

mkdir -p /tmp/dc && cd /tmp/dc && git init -q
printf '# API\n\n## Support matrix\n\nWe support Node 20. Must be exact.\n' > api.md
git add . && git commit -qm "seed docs" -m "Doc-Draft: human" -m "Source: src/platform.ts"
python3 /path/to/doc_ceiling.py api.md
# expect: ok 0.00 commitment Support matrix  (exit 0)

printf '# API\n\n## Support matrix\n\nWe support Node 20. Must be exact.\n\nAlso never panic.\n' > api.md
git commit -qam "model draft" -m "Doc-Draft: model"
python3 /path/to/doc_ceiling.py api.md 0.34
# expect: FAIL 0.40 commitment Support matrix  (exit 1)
Enter fullscreen mode Exit fullscreen mode

The second run flags 2 of 5 section lines as model-owned — 0.40, above the 0.34 ceiling, in a section that says "must". That is the whole mechanism. If your numbers differ, the line-order assumption in blame is wrong for your file and you should stop and fix it.

4. Wire it into pull requests

name: doc-ceiling
on: [pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: |
          git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- 'docs/**/*.md' \
            | xargs -r -n1 python3 tools/doc_ceiling.py
Enter fullscreen mode Exit fullscreen mode

fetch-depth: 0 is not optional; a shallow clone makes blame lie. And a red gate should mean "a human must edit this section", not "revert the model draft".

Decision table

Section Model drafts Human edits last Gate
Overview, motivation yes no none
Tutorial narration yes spot-check commands must exist in repo
Command reference generation yes, if flags change diff vs --help output
Support matrix no yes ceiling 0.00
Limits / pricing no yes, with Source: ceiling 0.00
Deprecation notice phrasing only yes, date and version ceiling 0.00
Changelog yes, from PR titles no links to merged PRs
Security notes no yes reviewer with context

Where the drafting lane runs

Bulk regeneration is a batch job: pull changed source files, draft narrative sections, open a PR with the trailers attached. That job needs a model endpoint and a machine to run it, which is the part people usually stop at during evaluation.

I used MonkeyCode's free model access and free server option as that drafting lane while putting this workflow together. The model fills the narrative sections and writes Doc-Draft: model; the server runs the regeneration script and the audit above before anything is pushed. Nothing about the gate changes if you swap the endpoint — that is the point of keeping provenance in git rather than in a prompt.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Two honest caveats. Free-tier availability and terms can change, so check current terms before you build a pipeline on them — this article does not verify quotas, model names, hardware, or duration. And a free drafting lane is still a drafting lane: it does not make a commitment sentence safe.

Where this breaks

  • Squash merges. One squashed commit erases per-line provenance in that PR. Either keep model and human edits in separate commits, or accept a coarser ceiling.
  • Rewrites by reformatting. A formatter touching every line makes the formatter commit the last owner of every sentence. Exclude generated paths.
  • Regex is dumb. "Supports dark mode" in a UI doc is not a compatibility commitment, but it trips the gate. Tune the token list per repo; false positives are cheaper than false negatives here.
  • It checks ownership, not truth. A human can own a wrong sentence. This gate proves accountability, not accuracy.
  • Line granularity. A one-word human edit to a model-generated paragraph scores 0.05 and looks fine while the surrounding claim stays model-shaped.

Who should not use it: teams that squash everything and cannot change that; docs living in a wiki or CMS with no git history; and regulated documentation where every sentence needs a named subject-matter reviewer. A ratio gate is not a substitute for that signature.

Rollout in four steps

  1. Pick one commitment-heavy file. Run the checker in report-only mode; do not gate yet.
  2. Add the Doc-Draft trailer to the next five docs commits by hand, and confirm blame resolves them correctly.
  3. Set ceiling 0.00 for matrices, limits, and security sections; 0.34 for narrative.
  4. Review the gate output weekly and delete token patterns that keep firing on harmless prose.

The model can write most of the words. Git is how you keep receipts on the ones that matter. If you want to see the loop end to end before committing your own repo, start with a throwaway branch and the free model access on offer — the gate will tell you quickly whether your trailers are telling the truth.

Top comments (0)