# Who Owns This Paragraph? An Ownership Manifest for AI-Drafted Docs

> Source: <https://dev.to/datago_7777/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs-ab6>
> Published: 2026-10-15 11:16:47+00:00

Here is a scenario that repeats in most repositories that let a model write documentation. A pull request adds a runbook section: rotate the API credential, then restart the worker. The diff is clean, the headings match the style guide, and the reviewer approves it in ninety seconds.

Six weeks later the steps are wrong. Rotation moved behind a queue, and the text now describes a procedure that no longer exists. Nobody can name the person who was supposed to catch that. The paragraph has an author. It has no owner.

That gap is the interesting part. Style is cheap to automate; accountability is not. What follows is a workflow that keeps generated prose draftable while keeping ownership human, plus the check that enforces the boundary in CI.

They fail on four smaller things:

Better prompts do not fix any of these. A contract next to the document does.

Treat a document as a set of addressable blocks. Each block gets one of three classes:

`source_ref` pointing into code, config, or schema.`human_attestation` reference.
A three-state enum is boring on purpose. It is easy to review and easy to test, and both of those matter more than expressiveness.

Blocks are delimited with HTML comments so they stay invisible when rendered:

``` php
<!-- block:rotation-steps -->
1. Drain the worker pool.
2. Rotate the credential in the secret store.
3. Restart the worker and confirm the health endpoint.
<!-- /block -->
```

The manifest then lists every block, its class, its owner, and its evidence:

```
# docs/ownership.yaml
version: 1
documents:
  - path: docs/ops/rotate-api-credentials.md
    blocks:
      - id: overview
        class: draftable
        owner: '@ops-docs'
        evidence: source_ref
        ref: 'services/auth/src/rotate.ts#L40-L96'
      - id: rotation-steps
        class: attestable
        owner: '@ops-oncall'
        evidence: human_attestation
        ref: 'ops-review-log#R-2214'
      - id: customer-impact
        class: prohibited
        owner: '@support-lead'
        evidence: human_attestation
        ref: 'reachability-review#R-88'
```

Paths and review-log identifiers above are illustrative placeholders; substitute your own.

One script is enough to turn the manifest into a failing check:

``` bash
#!/usr/bin/env python3
'''check_ownership.py — fail when a docs block has no owner or no evidence.'''
from __future__ import annotations

import json
import sys
from pathlib import Path

import yaml  # pip install pyyaml

REQUIRED = {'id', 'class', 'owner', 'evidence'}
CLASSES = {'draftable', 'attestable', 'prohibited'}
EVIDENCE = {'source_ref', 'command_output', 'human_attestation'}
NEEDS_REF = {'draftable', 'attestable'}

def block_ids(markdown: str) -> set[str]:
    '''Collect block markers: <!-- block:id --> ... <!-- /block -->'''
    found = set()
    for raw in markdown.splitlines():
        line = raw.strip()
        if line.startswith('<!-- block:') and line.endswith('-->'):
            found.add(line[len('<!-- block:'):-3].strip())
    return found

def check(manifest_path: Path) -> list[str]:
    errors: list[str] = []
    data = yaml.safe_load(manifest_path.read_text(encoding='utf-8'))
    if data.get('version') != 1:
        errors.append('manifest: unsupported version')

    for doc in data.get('documents', []):
        path = Path(doc['path'])
        if not path.exists():
            errors.append(f'{path}: declared but missing')
            continue

        declared = {b['id']: b for b in doc.get('blocks', [])}
        present = block_ids(path.read_text(encoding='utf-8'))

        for orphan in sorted(present - declared.keys()):
            errors.append(f'{path}: block {orphan!r} missing from manifest')
        for ghost in sorted(declared.keys() - present):
            errors.append(f'{path}: manifest declares {ghost!r}, no such block')

        for bid, block in declared.items():
            missing = REQUIRED - block.keys()
            if missing:
                errors.append(f'{path}#{bid}: missing {sorted(missing)}')
                continue
            cls = block['class']
            ev = block['evidence']
            if cls not in CLASSES:
                errors.append(f'{path}#{bid}: unknown class {cls!r}')
            if ev not in EVIDENCE:
                errors.append(f'{path}#{bid}: unknown evidence {ev!r}')
            if cls in NEEDS_REF and not block.get('ref'):
                errors.append(f'{path}#{bid}: class {cls} needs a ref')
    return errors

if __name__ == '__main__':
    target = Path(sys.argv[1] if len(sys.argv) > 1 else 'docs/ownership.yaml')
    failures = check(target)
    print(json.dumps({'failures': failures}, indent=2))
    sys.exit(1 if failures else 0)
```

A pytest wrapper makes it part of the normal test run:

``` python
# tests/test_docs_ownership.py
import subprocess
import sys

def test_ownership_manifest_is_satisfied():
    result = subprocess.run(
        [sys.executable, 'tools/check_ownership.py', 'docs/ownership.yaml'],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stdout + result.stderr
```

And the CI job is small enough to read in one screen:

```
name: docs-ownership
on:
  pull_request:
    paths:
      - 'docs/**'
      - 'tools/check_ownership.py'
      - 'tests/test_docs_ownership.py'
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install pyyaml pytest
      - run: python tools/check_ownership.py docs/ownership.yaml
      - run: pytest -q tests/test_docs_ownership.py
```

| Block class | Model may | Human must | Evidence key | CI behaviour | 
|---|---|---|---|---|
| draftable | write the first pass | verify every factual claim against `ref` | `source_ref` | fails without `ref` | 
| attestable | draft the skeleton only | run each step and record the attestation | `human_attestation` | fails without `ref` | 
| prohibited | nothing | write it or approve it explicitly | `human_attestation` | review only | 

One honest limit: detecting that a model *touched* a prohibited block requires your generator to record provenance. If it cannot, drop that claim and let review carry it rather than pretending the check proves it.

The manifest makes the draft step cheap and the gate non-negotiable. That split is where free infrastructure helps instead of getting in the way.

`draftable` and `attestable` blocks. MonkeyCode's free model access fits that shape, because the artifact is still a first pass a human signs.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Two caveats that matter more than the convenience. Quotas, limits, and availability on any free tier change, so check the current plan before routing a pipeline through it, and never treat a free tier as a production SLA. A cheap draft does not reduce what the human still owns.

`prohibited` and escalate.
You do not need to retrofit an entire docs tree. Pick one runbook, mark three blocks, write three manifest entries, and run the script once. If it passes on the first attempt, the manifest is usually too vague to be useful.

The rule worth keeping is short: the model may draft anything a human will read, and it may never own the sentence someone will act on. Keep the gate yours, and a free first pass is a reasonable place to begin.
