cd /news/ai-tools/who-owns-this-paragraph-an-ownership… · home › topics › ai-tools › article
[ARTICLE · art-150581] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

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

A developer proposes an ownership manifest for AI-drafted documentation that assigns every addressable block in a Markdown file one of three classes — draftable, attestable, or prohibited — each with a named human owner and evidence, and enforces the contract with a Python CI script (check_ownership.py) that fails when a block lacks an owner or evidence. The workflow uses HTML-comment block delimiters and a docs/ownership.yaml manifest to keep generated prose draftable while keeping accountability human.

by read5 min views1 publishedOct 15, 2026

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:

<!-- 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:

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:

#!/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:

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.

── more in #ai-tools 4 stories · sorted by recency
── more on @check_ownership.py 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/who-owns-this-paragr…] indexed:0 read:5min 2026-10-15 · —