DEV Community

Avery Lin
Avery Lin

Posted on

Path Ownership Ledgers: Model Drafts Versus Human-Reserved Docs

Generated documentation fails most often when a model writes claims a human must still own. The practical fix is a path-level ownership ledger checked in CI before any draft is merged. Model output may fill reference and API surfaces, while humans keep promises, policy, and incident language. The rest of this article specifies the ledger, a validator, and a drafting workflow that respects those boundaries.

The failure mode is completeness, not grammar

Most generated pages look finished because headings, tables, and examples arrive together. Completeness is the hazard: a model fills empty cells with plausible uptime numbers, support hours, and compatibility claims. Reviewers then argue tone while an invented SLA sits in the published tree. A ledger does not grade prose quality; it forbids certain paths and sentence shapes from leaving a draft job.

This is adjacent to agent workflows that keep assuming facts the operator never supplied. Documentation generators do the same thing when a section looks sparse. The correction is mechanical ownership, not a longer system prompt. Treat every output file as either model-draftable, human-reserved, or proposal-only until a person promotes it.

What a model may draft

A model may draft text that is recoverable from a repository artifact without adding a promise. Recoverable surfaces include CLI flag tables, OpenAPI field lists, config key catalogs, and error-code indexes. Those pages should cite a generator input such as a schema file, a --help dump, or a source comment block. If the input disappears, the page should fail CI rather than keep yesterday’s invented wording.

A model may also draft structural glue: section order, cross-links among reference pages, and short descriptions that restate identifiers already present in code. Glue is still a draft. It must not introduce product guarantees, legal posture, or root-cause language. Label generated files so later diffs show the machine boundary without reading the entire commit.

What a human must own

A human must own every sentence that would still be wrong if the code were correct. That set includes SLAs, pricing, support coverage, security certifications, data-retention periods, and incident narratives. It also includes deprecation calendars that bind customers, compatibility statements that name third-party products, and any “we guarantee” construction. Those claims survive refactors; they are contractual, not documentary.

Humans must also own the decision to publish. A proposal file is not a page. Promotion is a review action that copies or renames a draft onto a reserved path after a named reviewer signs the change. Automated merge of proposal files into reserved paths is a process bug, even when the prose looks careful.

Decision table for a docs tree

Use this table as the contract, not as a style guide. Rows are claim families; columns are allowed producers.

Claim family Model may draft Human must own Proposal file allowed
Identifier lists from schema or --help Yes Review only Optional
Parameter types and default literals in code Yes Review only Optional
Cross-links among generated reference pages Yes Review only Optional
Uptime, RTO, RPO, or support-hour numbers No Yes No
Pricing, packaging, or license grants No Yes No
Security certification and audit posture No Yes No
Incident timeline and root cause No Yes No
Customer-binding deprecation dates No Yes No
“How we think about X” narrative No Yes Yes, never auto-merged

The last row is the usual leak. Narrative tone invites a model to sound authoritative about values the company has not approved. Keep those pages human-authored even when a drafting job exists for reference trees.

Ledger format

Store ownership next to the docs tree so review happens in the same pull request. The YAML below is a proposal for a small CLI product; adjust globs, not the three roles.

# docs/ownership.yaml
version: 1
roles:
  model_may_draft:
    - docs/reference/cli/**
    - docs/reference/api/**
    - docs/generated/**
  human_must_own:
    - docs/legal/**
    - docs/security/posture.md
    - docs/support/sla.md
    - docs/support/hours.md
    - docs/incidents/**
    - docs/pricing.md
    - CHANGELOG.md
  model_may_propose:
    - docs/drafts/narrative/**
rules:
  require_header_on_generated: true
  forbid_promote_without_label: true
  reserved_phrase_scan:
    - docs/reference/**
    - docs/generated/**
    - docs/drafts/**
Enter fullscreen mode Exit fullscreen mode

Generated files should declare their role in a header the validator can parse. A missing header is a failure, not a warning, because unlabeled prose later looks human.

<!-- docs-ownership: model_may_draft input=openapi.yaml -->
# Widgets API

Field list recovered from `openapi.yaml`. Do not add uptime or support claims.
Enter fullscreen mode Exit fullscreen mode

Workflow: draft only on allowed paths

Follow the numbered sequence on every regeneration. Skipping a step is how reserved pages accrete invented numbers.

  1. Freeze the ownership file on the default branch before enabling any drafting job. Unowned paths default to human_must_own so new folders cannot silently become machine-written.
  2. Collect generator inputs as files: schema, command dumps, or extracted comment blocks. Do not paste human narrative into that bundle.
  3. Run the drafting job only against globs listed under model_may_draft or model_may_propose. Write outputs into those trees exclusively.
  4. Stamp each output with a docs-ownership header that names the input artifact. Reject files whose header role disagrees with the matching glob.
  5. Scan draft text for reserved phrases even on allowed paths. A CLI reference that mentions “99.9%” is still a policy leak.
  6. Open a pull request that contains drafts, the scan report, and no edits under human_must_own except by a human author.
  7. Promote a proposal file only with a review label such as docs-owned-by:@reviewer. The validator should block merges that lack it.

When a team wants a cheap loop for regenerating reference pages, MonkeyCode’s free model access and free server option can host the draft-and-validate job. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Keep the ledger and validator in your repository so the same checks run if the drafting host changes.

Validator: paths, headers, and promotions

The script below is labeled as a working sketch. Run it from the repository root in CI. It does not call a model; it only enforces the ledger.

# tools/validate_docs_ownership.py
from __future__ import annotations

import pathlib
import re
import sys

import yaml

HEADER_RE = re.compile(
    r"<!--\s*docs-ownership:\s*(model_may_draft|model_may_propose|human_must_own)"
    r"(?:\s+input=(\S+))?\s*-->"
)
PROMOTE_LABEL = "docs-owned-by:"


def load_ledger(root: pathlib.Path) -> dict:
    path = root / "docs" / "ownership.yaml"
    with path.open(encoding="utf-8") as handle:
        return yaml.safe_load(handle)


def glob_match(path: str, patterns: list[str]) -> bool:
    posix = pathlib.PurePosixPath(path)
    return any(posix.match(pattern) for pattern in patterns)


def role_for(rel: str, ledger: dict) -> str:
    roles = ledger["roles"]
    if glob_match(rel, roles["human_must_own"]):
        return "human_must_own"
    if glob_match(rel, roles["model_may_draft"]):
        return "model_may_draft"
    if glob_match(rel, roles["model_may_propose"]):
        return "model_may_propose"
    return "human_must_own"


def changed_markdown(root: pathlib.Path) -> list[pathlib.Path]:
    # CI should pass a file list; this fallback scans the docs tree.
    return [p for p in (root / "docs").rglob("*.md") if p.is_file()]


def main() -> int:
    root = pathlib.Path(".").resolve()
    ledger = load_ledger(root)
    errors: list[str] = []
    for path in changed_markdown(root):
        rel = path.relative_to(root).as_posix()
        expected = role_for(rel, ledger)
        text = path.read_text(encoding="utf-8")
        match = HEADER_RE.search(text)
        if expected == "human_must_own":
            if match and match.group(1) != "human_must_own":
                errors.append(f"{rel}: generated header on reserved path")
            continue
        if ledger["rules"].get("require_header_on_generated") and not match:
            errors.append(f"{rel}: missing docs-ownership header")
            continue
        if match and match.group(1) != expected:
            errors.append(f"{rel}: header {match.group(1)} != {expected}")
        if expected == "model_may_draft" and match and not match.group(2):
            errors.append(f"{rel}: model_may_draft requires input=")
        if expected == "model_may_propose" and PROMOTE_LABEL not in text:
            if path.name.endswith(".md") and "/drafts/" not in rel:
                errors.append(f"{rel}: proposal left reserved tree without owner label")
    if errors:
        print("ownership check failed:")
        print("\n".join(errors))
        return 1
    print("ownership check passed")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Install PyYAML in the CI image, then run a deterministic command. Do not pipe model output into this script’s stdin; the files on disk are the contract.

pip install pyyaml
python tools/validate_docs_ownership.py
Enter fullscreen mode Exit fullscreen mode

Reserved phrase scan on allowed paths

Path ownership is necessary and still insufficient. A model can leak policy into a legal-looking sentence inside docs/reference/cli/. Scan those trees with patterns that almost never belong in recovered reference text. Tune the list with legal and support owners; do not treat it as a model card.

# tools/scan_reserved_phrases.py
from __future__ import annotations

import pathlib
import re
import sys

PATTERNS = [
    re.compile(r"\b(99\.9|99\.99|99\.999)\s*%"),
    re.compile(r"\b(SLA|RTO|RPO)\b", re.I),
    re.compile(r"\bwe guarantee\b", re.I),
    re.compile(r"\bSOC\s*2\b", re.I),
    re.compile(r"\bISO\s*27001\b", re.I),
    re.compile(r"\b24\s*/\s*7\b"),
    re.compile(r"\broot cause\b", re.I),
    re.compile(r"\bwe are HIPAA compliant\b", re.I),
]

TARGETS = [
    "docs/reference",
    "docs/generated",
    "docs/drafts",
]


def main() -> int:
    root = pathlib.Path(".").resolve()
    hits: list[str] = []
    for folder in TARGETS:
        base = root / folder
        if not base.exists():
            continue
        for path in base.rglob("*.md"):
            for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
                for pattern in PATTERNS:
                    if pattern.search(line):
                        rel = path.relative_to(root).as_posix()
                        hits.append(f"{rel}:{i}: {pattern.pattern}: {line.strip()}")
    if hits:
        print("reserved phrase scan failed:")
        print("\n".join(hits))
        return 1
    print("reserved phrase scan passed")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

A hit is not proof of legal risk; it is proof the draft left the recoverable surface. Move the sentence to a human-owned page or delete it. Do not “soften” an SLA percentage in a CLI reference until the scan passes.

CI wiring without extra product claims

Keep the job boring. Generate reference pages in one step, then fail the pipeline on ownership or phrase violations. The example uses generic commands so the host can be a laptop, a shared runner, or a free server option used only as compute.

# .github/workflows/docs-ownership.yml
name: docs-ownership
on:
  pull_request:
    paths:
      - "docs/**"
      - "tools/validate_docs_ownership.py"
      - "tools/scan_reserved_phrases.py"
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pyyaml
      - run: python tools/validate_docs_ownership.py
      - run: python tools/scan_reserved_phrases.py
Enter fullscreen mode Exit fullscreen mode

If the drafting job runs elsewhere, still require these two commands on the pull request that lands files. Host substitution must not skip the ledger. Record the generator input filename in the header so stale pages are visible when the schema moves.

Limitations

The ledger cannot see meaning. A model can invent a support policy using words that dodge the regex list, and the path check will still pass. Phrase scans drift as marketing copy changes, so owners must revise patterns when a new promise vocabulary appears. Header stamps can be forged by any committer; they are coordination tools, not cryptography.

Globs also collide. A file matching both human_must_own and model_may_draft is treated as reserved in the sketch, which is conservative and sometimes noisy. Nested products with mixed trees will need longer pattern lists and tests for the matcher. The workflow assumes Markdown files; binary screenshots and generated HTML need a separate rule.

This article does not report latency, quality scores, or model rankings. Those numbers would be invented here. Measure your own false-positive rate on a week of pull requests before enforcing the scan on every branch.

Who should not use this approach

Skip the ledger if documentation is entirely human-written and never machine-emitted. The headers become ceremony without a drafting job. Skip it for attorney-held corpora where every syllable is already under outside counsel; a glob file is not a privilege protocol. Skip it when the “docs” tree is a wiki without pull requests, because the validator has no merge gate.

Teams that generate only internal scratch notes can use a simpler ignore folder. The cost of this method is review discipline, not model access. If nobody will reject a green CI that still contains a customer-facing promise, the YAML will not save the page.

Closing

Own the paths first, then allow models to fill recoverable surfaces inside that fence. Keep promises, policy, and incidents off the drafting glob even when the prose looks complete. If you already run docs CI, add the ledger to one reference tree and read the first week of scan failures before expanding coverage.

Top comments (0)