{"slug": "who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs", "title": "Who Owns This Paragraph? An Ownership Manifest for AI-Drafted Docs", "summary": "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.", "body_md": "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.\n\nSix 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.\n\nThat 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.\n\nThey fail on four smaller things:\n\nBetter prompts do not fix any of these. A contract next to the document does.\n\nTreat a document as a set of addressable blocks. Each block gets one of three classes:\n\n`source_ref` pointing into code, config, or schema.`human_attestation` reference.\nA three-state enum is boring on purpose. It is easy to review and easy to test, and both of those matter more than expressiveness.\n\nBlocks are delimited with HTML comments so they stay invisible when rendered:\n\n``` php\n<!-- block:rotation-steps -->\n1. Drain the worker pool.\n2. Rotate the credential in the secret store.\n3. Restart the worker and confirm the health endpoint.\n<!-- /block -->\n```\n\nThe manifest then lists every block, its class, its owner, and its evidence:\n\n```\n# docs/ownership.yaml\nversion: 1\ndocuments:\n  - path: docs/ops/rotate-api-credentials.md\n    blocks:\n      - id: overview\n        class: draftable\n        owner: '@ops-docs'\n        evidence: source_ref\n        ref: 'services/auth/src/rotate.ts#L40-L96'\n      - id: rotation-steps\n        class: attestable\n        owner: '@ops-oncall'\n        evidence: human_attestation\n        ref: 'ops-review-log#R-2214'\n      - id: customer-impact\n        class: prohibited\n        owner: '@support-lead'\n        evidence: human_attestation\n        ref: 'reachability-review#R-88'\n```\n\nPaths and review-log identifiers above are illustrative placeholders; substitute your own.\n\nOne script is enough to turn the manifest into a failing check:\n\n``` bash\n#!/usr/bin/env python3\n'''check_ownership.py — fail when a docs block has no owner or no evidence.'''\nfrom __future__ import annotations\n\nimport json\nimport sys\nfrom pathlib import Path\n\nimport yaml  # pip install pyyaml\n\nREQUIRED = {'id', 'class', 'owner', 'evidence'}\nCLASSES = {'draftable', 'attestable', 'prohibited'}\nEVIDENCE = {'source_ref', 'command_output', 'human_attestation'}\nNEEDS_REF = {'draftable', 'attestable'}\n\ndef block_ids(markdown: str) -> set[str]:\n    '''Collect block markers: <!-- block:id --> ... <!-- /block -->'''\n    found = set()\n    for raw in markdown.splitlines():\n        line = raw.strip()\n        if line.startswith('<!-- block:') and line.endswith('-->'):\n            found.add(line[len('<!-- block:'):-3].strip())\n    return found\n\ndef check(manifest_path: Path) -> list[str]:\n    errors: list[str] = []\n    data = yaml.safe_load(manifest_path.read_text(encoding='utf-8'))\n    if data.get('version') != 1:\n        errors.append('manifest: unsupported version')\n\n    for doc in data.get('documents', []):\n        path = Path(doc['path'])\n        if not path.exists():\n            errors.append(f'{path}: declared but missing')\n            continue\n\n        declared = {b['id']: b for b in doc.get('blocks', [])}\n        present = block_ids(path.read_text(encoding='utf-8'))\n\n        for orphan in sorted(present - declared.keys()):\n            errors.append(f'{path}: block {orphan!r} missing from manifest')\n        for ghost in sorted(declared.keys() - present):\n            errors.append(f'{path}: manifest declares {ghost!r}, no such block')\n\n        for bid, block in declared.items():\n            missing = REQUIRED - block.keys()\n            if missing:\n                errors.append(f'{path}#{bid}: missing {sorted(missing)}')\n                continue\n            cls = block['class']\n            ev = block['evidence']\n            if cls not in CLASSES:\n                errors.append(f'{path}#{bid}: unknown class {cls!r}')\n            if ev not in EVIDENCE:\n                errors.append(f'{path}#{bid}: unknown evidence {ev!r}')\n            if cls in NEEDS_REF and not block.get('ref'):\n                errors.append(f'{path}#{bid}: class {cls} needs a ref')\n    return errors\n\nif __name__ == '__main__':\n    target = Path(sys.argv[1] if len(sys.argv) > 1 else 'docs/ownership.yaml')\n    failures = check(target)\n    print(json.dumps({'failures': failures}, indent=2))\n    sys.exit(1 if failures else 0)\n```\n\nA pytest wrapper makes it part of the normal test run:\n\n``` python\n# tests/test_docs_ownership.py\nimport subprocess\nimport sys\n\ndef test_ownership_manifest_is_satisfied():\n    result = subprocess.run(\n        [sys.executable, 'tools/check_ownership.py', 'docs/ownership.yaml'],\n        capture_output=True,\n        text=True,\n    )\n    assert result.returncode == 0, result.stdout + result.stderr\n```\n\nAnd the CI job is small enough to read in one screen:\n\n```\nname: docs-ownership\non:\n  pull_request:\n    paths:\n      - 'docs/**'\n      - 'tools/check_ownership.py'\n      - 'tests/test_docs_ownership.py'\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      - run: pip install pyyaml pytest\n      - run: python tools/check_ownership.py docs/ownership.yaml\n      - run: pytest -q tests/test_docs_ownership.py\n```\n\n| Block class | Model may | Human must | Evidence key | CI behaviour | \n|---|---|---|---|---|\n| draftable | write the first pass | verify every factual claim against `ref` | `source_ref` | fails without `ref` | \n| attestable | draft the skeleton only | run each step and record the attestation | `human_attestation` | fails without `ref` | \n| prohibited | nothing | write it or approve it explicitly | `human_attestation` | review only | \n\nOne 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.\n\nThe manifest makes the draft step cheap and the gate non-negotiable. That split is where free infrastructure helps instead of getting in the way.\n\n`draftable` and `attestable` blocks. MonkeyCode's free model access fits that shape, because the artifact is still a first pass a human signs.\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nTwo 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.\n\n`prohibited` and escalate.\nYou 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.\n\nThe 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.", "url": "https://wpnews.pro/news/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs", "canonical_source": "https://dev.to/datago_7777/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs-ab6", "published_at": "2026-10-15 11:16:47+00:00", "updated_at": "2026-10-15 11:19:21.276473+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "mlops"], "entities": ["check_ownership.py"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs", "markdown": "https://wpnews.pro/news/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs.md", "text": "https://wpnews.pro/news/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs.txt", "jsonld": "https://wpnews.pro/news/who-owns-this-paragraph-an-ownership-manifest-for-ai-drafted-docs.jsonld"}}