Freeze a Reader Contract Before Models Draft Tutorial Pages
Generated tutorial pages stay trustworthy only when a human freezes audience, prerequisites, and success checks before drafting begins. Models may draft connective prose around those owned facts, but they should not invent the facts themselves. This article defines an ownership split, a contract file, and a checker that rejects drafts carrying assumptions the contract never listed. The checker is a proposed reproducible example, not a measured production system with claimed pass rates.
Why reader state fails before grammar does
Tutorial failures often begin with a silent assumption, not with awkward wording or a missing comma. A draft may name an operating system, a language version, a time budget, or a success string that nobody verified. Those details feel helpful because they make the page look finished, yet they change what a newcomer will attempt. Diátaxis treats a tutorial as a learning path that must stay reliable, so preconditions belong to an owner who can check them.
Authors who want the source framing can read the Diátaxis tutorial section at https://diataxis.fr/tutorials/ before editing the contract. Recent public discussion of generated software points at the same gap, without prescribing a documentation control. People can produce a page quickly, and that page can still omit the boring constraints that make the instructions true.
This workflow does not score models, and it does not claim a measured documentation error rate from any survey. It only separates owned reader facts from draftable explanation, so review time lands on facts that can mislead a newcomer. The method is intentionally narrow, and it leaves command inventories and escalation pages to their existing owners.
Decide what the model may draft
The table is the control surface for this workflow, and every later step assumes those permissions stay unchanged. A human edits the contract file, while a model may quote contract lines and may draft sections marked as prose. A model may not add a prerequisite, a duration, a version, a supported platform, or a success string.
Commands, escalation contacts, and product limits stay outside this page, because they need separate owned artifacts. If a sentence states a pager target, a new shell command, or a numeric limit, the draft fails closed for human review. Reviewers should reject mixed pages, rather than hoping a later prose edit will sort ownership after the draft lands.
| Block | Owner | Model permission |
|---|---|---|
| Audience and goal | Human | Quote the contract line only |
| Prerequisites and environment | Human | Quote allowed items only |
| Success checks | Human | Quote allowed strings only |
| Non-goals | Human | Quote the list only |
| Step-order rationale | Model | Draft, without new facts |
| Transitions between owned steps | Model | Draft, without new commands |
| Analogies and orientation | Model | Draft, if no new claims appear |
Step 1: Extract reader facts from files you already trust
Start from files a reviewer can open, such as a language version pin, a CI image name, and the install notes. Copy only facts you can point to, and record the source path beside each fact in the contract. Leave unknown fields empty rather than asking a model to invent a plausible reader, platform, or outcome. An empty field is a review task for a human owner, not a generation opportunity for the drafting model.
# reader_contract.yml — human-owned; models may quote, never extend
contract_version: 1
audience: 'A developer who can run git and a shell, and who has not deployed this repo before'
goal: 'Complete the local demo and match one owned success string'
sources:
- path: '.python-version'
fact: 'Python version pin lives in this file'
- path: '.github/workflows/docs.yml'
fact: 'CI doc job name is the environment reference'
allowed_platforms:
- 'Linux'
allowed_prerequisites:
- 'git'
- 'a Python install matching .python-version'
forbidden_additions:
- 'new package managers'
- 'cloud accounts'
- 'timing promises'
success_checks:
- id: 'banner'
expect: 'demo ready'
non_goals:
- 'production deployment'
- 'support commitments'
draftable_sections:
- 'why-this-order'
- 'orientation'
quote_only_sections:
- 'audience'
- 'prerequisites'
- 'success'
Treat the YAML above as a proposed artifact, and change every path so it matches your repository before anyone relies on it. Do not paste secrets, customer names, or internal hostnames into the contract, even when a draft seems to need them. The contract should describe reader state and review sources, not grant access to machines, accounts, or private data.
Step 2: Mark sections before you call a model
Create the tutorial skeleton with headings that match the draftable list and the quote-only list in the contract. Put the contract's exact lines under quote-only headings, and leave draftable headings empty or filled with a placeholder. The model receives the contract, the skeleton, and a written limit against new platforms, versions, durations, commands, or success text. If the model returns a new heading or a new prerequisite, discard that output and rerun against the same frozen skeleton.
## audience
> owned: A developer who can run git and a shell, and who has not deployed this repo before
## prerequisites
> owned: git
> owned: a Python install matching .python-version
## success
> owned: demo ready
## why-this-order
DRAFT
## orientation
DRAFT
This ordering matters more than the exact wording of the prompt you send with the skeleton. A model that sees an empty page will often complete it, while a frozen contract leaves fewer gaps to fill. You still review the result, because a prompt is a request and not an enforcement mechanism by itself. Enforcement starts when the checker reads both files and exits non-zero on any smuggled reader fact.
Step 3: Reject drafts that smuggle new reader state
The Python below is a small unexecuted example you can run after you adapt the patterns to your repository. It flags platform names, duration phrases, version-like tokens, and success lines that the contract does not allow. It will miss clever paraphrases, so a green run is necessary and still not sufficient for publication.
#!/usr/bin/env python3
'''Proposed checker: fail if a tutorial draft adds reader facts.'''
import sys
from pathlib import Path
try:
import yaml
except ImportError:
sys.exit('install pyyaml in the review environment before running')
contract = yaml.safe_load(Path('reader_contract.yml').read_text())
draft = Path('docs/tutorial.md').read_text()
allowed = {item.lower() for item in contract['allowed_platforms']}
successes = {item['expect'] for item in contract['success_checks']}
platforms = {'linux', 'macos', 'windows', 'bsd'}
units = {'second', 'seconds', 'minute', 'minutes', 'hour', 'hours'}
errors = []
def prose_lines(text):
fenced = False
for line in text.splitlines():
if line.strip().startswith('```
'):
fenced = not fenced
continue
if not fenced:
yield line
body = '\n'.join(prose_lines(draft))
cleaned = ''.join(ch if ch.isalnum() else ' ' for ch in body.lower())
padded = ' ' + ' '.join(cleaned.split()) + ' '
for name in sorted(platforms - allowed):
if (' ' + name + ' ') in padded:
errors.append('platform not in contract: ' + name)
words = cleaned.split()
for left, right in zip(words, words[1:]):
if left.isdigit() and right in units:
errors.append('duration appeared near: ' + left + ' ' + right)
break
for line in prose_lines(draft):
if line.startswith('>') and 'owned:' in line:
continue
for token in line.replace(',', ' ').split():
parts = token.strip('`.').split('.')
if len(parts) >= 2 and all(part.isdigit() for part in parts[:2]):
errors.append('version-like token outside owned quote: ' + line.strip())
break
for line in prose_lines(draft):
if 'you should see' not in line.lower():
continue
if not any(expected in line for expected in successes):
errors.append('success line is not an owned check: ' + line.strip())
if errors:
print('\n'.join(errors))
sys.exit(1)
print('reader contract check passed')
```
Store the script next to the contract so the review command does not depend on a private notebook or a chat transcript. Run the checker with an ordinary review command, and keep the contract beside the draft in the same change. Publish the pair only after the command exits zero and a human has read both diffs.
``{% endraw %}{% raw %}`bash
python3 -m pip install pyyaml
python3 check_reader_contract.py
```
A failing run should name the smuggled fact, and it should not attempt to rewrite the tutorial page. Update the contract if the fact is real and sourced, or delete the sentence if the model invented it. Do not clear a red check by widening the allowlist until it merely matches the generated draft.
## Step 4: Review the contract hash separately from the prose
Record the contract hash in the pull request so reviewers can see whether reader facts actually moved. Prose-only edits should leave that hash unchanged, while fact edits should change the hash and cite a source path. Ask one reviewer to approve the contract diff, and ask another to read draftable sections for leftover claims. A single reviewer can cover both roles on a small page, but the two diffs should stay visually separate.
``{% endraw %}{% raw %}`bash
sha256sum reader_contract.yml | tee contract.sha256
git diff -- reader_contract.yml docs/tutorial.md
```
Use the hash as a routing signal for review, not as a signature from a release manager or a lawyer. A matching hash only means the contract bytes did not change, and it says nothing about whether the prose stayed faithful. If the hash changed and the description says the edit is wording only, stop and split the change again.
## Step 5: Keep the free draft lane narrower than the contract
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access can draft the sections marked draftable, and the free server option can run the checker for this narrow job. Those two availability claims are the only product facts this workflow uses, and they were supplied for this draft. This article does not name a model, a quota, a machine size, a retention period, or a benchmark result.
Read current product terms before you depend on the free lane, because this page does not track live availability. You edit the contract on your machine, then send the skeleton and the contract to the free model for prose only. You run the checker on the free server, or locally if that is simpler, and you treat a green check as review permission only. A green check is not permission to publish, because paraphrased inventions can still pass a lexical scan.
``{% endraw %}{% raw %}`bash
python3 check_reader_contract.py && python3 -m http.server 8080 --directory docs
```
If the free lane is unavailable, the same files still run on any Python 3 environment that has PyYAML installed. The preview command above assumes only a shell where Python 3 is already installed, and it sets no product-specific flags. Stop the preview when the review ends, and do not treat an open port as part of the documentation contract. If a server policy forbids that preview, rely on the checker exit code and a normal diff instead.
Keep product language out of the contract file, because the contract should remain useful if you change drafting tools. Reviewers should be able to run the checker without knowing which editor produced the draftable sections. The ownership split, not the vendor name, is the part worth copying into another documentation repository.
## Read a constructed failure before you trust a green check
The following failure text is a constructed illustration, not the output of a live run against a hosted model. It shows the kind of sentence the checker should catch before a reviewer spends time on tone. Keep constructed examples in tests or in review notes, and do not commit them as if they were the live tutorial.
``{% endraw %}{% raw %}`text
On macOS, wait 5 minutes, then you should see server started.
```
That line adds a platform, a duration, and a success string, and none of those items exist in the sample contract. The correct repair is deletion or a sourced contract edit, not a prompt that asks the model to try again blindly. After the repair, rerun the checker and confirm that the contract hash changed only if a human added a sourced fact.
## Update the contract when the repository changes
When the CI image or the language pin changes, update the contract sources in the same pull request as the infrastructure change. A tutorial that quotes yesterday's pin will pass a careless read and still fail on a clean machine tomorrow. Add a path-existence check beside the lexical checker if you want missing source files to fail the review as well. Keep that second check mechanical, and do not let a model rewrite the contract while it drafts orientation prose.
```python
missing = [
item['path']
for item in contract['sources']
if not Path(item['path']).exists()
]
if missing:
raise SystemExit('missing source paths: ' + ', '.join(missing))
```
## Limitations to keep beside the checker
This checker is lexical, so a sentence can introduce a cloud account or a false outcome without matching the patterns. Extend the patterns when you see a repeated miss, and keep those patterns in version control beside the contract. The contract itself can be wrong, especially after CI changes if nobody updates the listed source paths. A green tutorial that nobody has executed remains an untested tutorial, regardless of how clean the prose looks.
Quoted lines can drift from source files when someone edits the YAML by hand and forgets the cited path. Do not treat this design as a compliance guarantee, a security boundary, or a substitute for a clean-machine run. The design only blocks one class of invented reader state, and other documentation risks still need their own owners.
## Who should not use this approach
Skip this workflow if nobody on the team can verify prerequisites against the repository and the CI configuration. Also skip it for pages that create support duties, legal terms, or billing claims, because those sentences need a different owner. Pure reference pages, where nearly every line is an extracted signature, fit an extraction check better than a reader contract. Teams that want the model to discover the audience should not adopt a method whose first rule forbids that discovery.
Place this contract on the next tutorial pull request, then see whether reviewers spend time on reader facts instead of sentence polish. Keep the file only if reviewers actually open it while they review the next tutorial pull request. Drop the contract if reviewers stop opening it, because an unread gate is only another file in the repository.
Top comments (0)