Concept pages stay accurate only when a human claim register exists before any model writes prose. A model may connect those approved claims, but it may not invent behavior, limits, or support facts. The register remains the only source of truth, while the drafted page functions only as an explanation layer. A small checker should fail the page when a risk marker, a bad citation, or an extra heading escapes that register.
Why concept pages drift faster than reference pages
Reference pages can be checked against signatures, schemas, and captured commands that already have separate review gates. Concept pages lack that anchor, so a model fills gaps with plausible architecture that no owner reviewed. Readers then treat smooth prose as evidence, even when the underlying system never implemented the described path. The failure is not stylistic, because it is an unowned factual claim that later pages copy forward.
A useful response is narrower than a general ban on letting models draft documentation at all. Teams still need readable transitions, audience framing, and short explanations of behavior that owners already decided. Those sentences are safe only when each factual clause points at a claim a human already accepted. Without that pointer, review becomes a reread of the whole page instead of a check of new identifiers.
What a human must own before drafting starts
The human owner publishes a claim register beside the page, and the model receives that file as input rather than as a suggestion. Each claim carries an identifier, one statement, a source path, a kind, and a status that review can query. The statement should be short enough to cite, and broad enough that prose does not need a second hidden fact. Status values stay limited to active, retired, and draft, with draft claims blocked from public pages.
The same owner also freezes three companion lists that drafting models tend to expand on their own. An outline lists the headings the page may use, in order, with no extra sections permitted. A forbidden list names words that imply guarantees, quotas, or support promises the register does not contain. A non-goal note states that procedures, changelogs, and troubleshooting steps live in other owned artifacts instead.
Numbers, durations, version ranges, and customer-visible limits stay inside claim statements rather than inside model prose. If a claim lacks a source path that a reviewer can open, it does not enter the register. Product names that are not already in the register are treated as new claims, even when they appear only in a comparison clause.
What the model may draft, and only that
The model may write orientation sentences that repeat a claim statement and attach its identifier in a fixed token. It may add a transition between two cited claims when the transition introduces no new behavior. It may write a short analogy only when that analogy contains no risk marker and no forbidden word. Readers should still see an explicit analogy label, because the checker does not score rhetorical intent.
It may not add headings, reorder the outline, or cite a claim whose status is not active. Examples, request bodies, and expected outputs remain human-owned, because a fluent example is still a behavior claim. The model may refer to an example only by an identifier that the register already lists. It may not invent a sample value, a status code, or a field name that the example file does not contain.
That split keeps the explanation readable without letting the drafted page become a second, competing specification. The matrix below is the operating rule for this page type, and the checker implements only the rightmost column. Rows marked human-owned are inputs to the prompt, not blanks the model is allowed to fill. A conflict between a row and a fluent draft is resolved by rejecting the draft, not by editing the row.
| Element | Human must own | Model may draft | Fence result |
|---|---|---|---|
| Claim statement and source | Yes | Quote it with an active id | Unknown id fails |
| Outline heading | Yes | Copy the approved text only | Extra heading fails |
| Transition without a new fact | No | Link two cited claims | Passes without a risk marker |
| Number, duration, or limit | Yes, inside a claim | No new value | Uncited digit fails |
| Example payload | Yes, as a file | Cite the example id only | Unused id fails |
| Forbidden word | The deny list | Not at all | Substring match fails |
| Labeled analogy | The label rule | No new fact and no deny word | Risk markers still fail |
A proposed fence, not a measured benchmark
Save the checker below as a repository script and treat it as a proposed filter, not as a benchmarked product. Five failure classes are enough for the first fence: bad citations, unused claims, extra headings, deny-list hits, and uncited risk markers. A risk marker is a digit, a version-shaped token, or a modal such as must, always, or never. Install PyYAML into the same interpreter you will use for the fence before you run either subcommand.
python3 -m pip install --user PyYAML
#!/usr/bin/env python3
"""Proposed claim fence. Unexecuted example, not a measured run.
Does not detect a cited sentence that distorts its claim.
"""
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
sys.exit("PyYAML is required")
CITE = re.compile(r"\{\{claim:([a-z0-9._-]+)\}\}")
SUSPECT = re.compile(
r"\b(?:must|always|never|guarantee|unlimited|\d+|v\d+)\b",
re.IGNORECASE,
)
HEAD = re.compile(r"^#{1,6}\s+(.*\S)\s*$", re.M)
def load(path):
return yaml.safe_load(Path(path).read_text())
def active_claims(reg):
return {
c["id"]: c
for c in reg.get("claims", [])
if c.get("status") == "active"
}
def sentences(text):
parts = re.split(r"(?<=[.!?])\s+", text.strip())
return [p for p in parts if p]
def export_prompt(reg, dest):
lines = [f"Page: {reg['page_id']}", "Use only these headings:"]
lines += [f"- {h}" for h in reg.get("outline", [])]
lines.append("Cite each fact with {{claim:id}}. Do not add claims.")
lines.append("Active claims:")
for cid, c in active_claims(reg).items():
lines.append(f"- {cid}: {c['statement']}")
lines.append("Forbidden words:")
lines += [f"- {w}" for w in reg.get("forbidden", [])]
Path(dest).write_text("\n".join(lines) + "\n")
def check(reg, draft):
errors = []
allowed = set(reg.get("outline", []))
for heading in HEAD.findall(draft):
if heading not in allowed:
errors.append(f"heading:{heading}")
known = active_claims(reg)
for token in reg.get("forbidden", []):
if token.lower() in draft.lower():
errors.append(f"forbidden:{token}")
for sent in sentences(draft):
cites = CITE.findall(sent)
for cid in cites:
if cid not in known:
errors.append(f"missing:{cid}")
if SUSPECT.search(sent) and not cites:
errors.append(f"uncited:{sent[:80]}")
used = set(CITE.findall(draft))
for cid in sorted(set(known) - used):
errors.append(f"unused:{cid}")
return errors
def main():
if len(sys.argv) != 4 or sys.argv[1] not in {"export", "check"}:
sys.exit("usage: claim_fence.py export|check REGISTER PATH")
reg = load(sys.argv[2])
if sys.argv[1] == "export":
export_prompt(reg, sys.argv[3])
print("exported")
return 0
draft = Path(sys.argv[3]).read_text()
errors = check(reg, draft)
if errors:
print("\n".join(errors))
return 1
print(f"ok claims={len(set(CITE.findall(draft)))}")
return 0
if __name__ == "__main__":
sys.exit(main())
Workflow in five numbered steps
Follow the steps in order, and stop when a step fails rather than patching prose by hand in the model session. The commands below describe a proposed local workflow, not a record of a measured run. Replace each sample path with the real repository layout that your documentation toolchain already uses.
1. Pin the claim register
Create the claim register file and review every statement change in the same pull request as the prose. Keep one page identifier per file so a draft cannot borrow claims from a neighboring concept. Record the review date as data, not as a sentence the model is free to update. Treat the sample date and source paths as illustrations, not as evidence that a team finished this review.
page_id: concept.session-lifecycle
owner: docs-platform
reviewed_on: 2026-10-12
outline:
- What a session is
- How login and logout relate
- What this page does not cover
claims:
- id: session.created_on_login
statement: "A session record is written only after login succeeds."
source: "internal/auth/session.go"
kind: behavior
status: active
- id: session.revoked_on_logout
statement: "Logout marks that session revoked for later refresh attempts."
source: "internal/auth/session.go"
kind: behavior
status: active
- id: session.example.login
statement: "The login example file is the only sample for this page."
source: "docs/examples/login.json"
kind: example
status: active
- id: session.page.scope
statement: "This page does not define setup commands, changelog text, or troubleshooting branches."
source: "docs/claims/session-lifecycle.yaml"
kind: scope
status: active
forbidden:
- "unlimited"
- "guarantee"
- "SLA"
- "always available"
non_goals:
- "setup commands"
- "changelog entries"
- "troubleshooting branches"
2. Freeze the outline as the only heading source
Ask the drafter to copy headings from the outline and to refuse any other heading level. A heading is a claim about scope, so an extra heading is an unowned topic that review did not accept. Store that rule in the prompt file that sits next to the register, and pass both files in every run. Pass the register to the export subcommand, and keep the generated prompt beside the register for review.
python3 scripts/claim_fence.py export \
docs/claims/session-lifecycle.yaml \
/tmp/concept-prompt.md
The exporter, which you should keep under review, prints the outline, active claim statements, and forbidden words. It does not ask the model to invent missing claims when the register is too thin to publish. A thin register means the page is not ready, not that the model should complete it.
3. Draft the explanation layer from the register alone
Send the exported prompt to a drafting model, and require every factual sentence to include a claim token from the register. Tell the model to return Markdown only, using the frozen headings, and to leave a TODO when a needed claim is absent. Do not paste production logs, customer names, or unpublished limits into that drafting prompt at all.
## What a session is
A session record is written only after login succeeds {{claim:session.created_on_login}}.
The login example file is the only sample for this page {{claim:session.example.login}}.
## How login and logout relate
Logout marks that session revoked for later refresh attempts {{claim:session.revoked_on_logout}}.
## What this page does not cover
This page does not define setup commands, changelog text, or troubleshooting branches {{claim:session.page.scope}}.
The sample stays deliberately plain, because color and emphasis are not the actual review target. The actual review target is whether each factual sentence maps cleanly onto exactly one active claim. If the model returns a suspicious sentence without a token, the checker should reject the file before tone review starts. A TODO is a stop sign for the human owner, not an invitation for a second unguided pass.
4. Run the fence before anyone edits prose
Run the check subcommand on the full draft, and treat a non-zero exit as a blocked review rather than a tone problem. Unused active claims are failures too, because a dropped claim hides behavior the owner already promised to explain. Forbidden words fail even inside analogies, since readers rarely notice the label wrapped around a guarantee. The following negative check is a proposed test, and a correct fence exits non-zero on the uncited digit and the unused claims.
python3 scripts/claim_fence.py check \
docs/claims/session-lifecycle.yaml \
docs/drafts/session-lifecycle.md
printf '## What a session is\n\nSessions last 30 minutes.\n' > /tmp/bad-concept.md
python3 scripts/claim_fence.py check \
docs/claims/session-lifecycle.yaml \
/tmp/bad-concept.md
5. Review the diff by claim identifier, then accept prose
Open the pull request with the register diff and the draft diff visible in one review view. Accept a wording change only when the cited identifiers stay stable and the checker is green. If a sentence needs a new fact, add the claim, name its source, and obtain human review before you regenerate the paragraph. Do not let the model edit the register file in the same turn that it edits prose.
Where a free drafting pass and a free server fit
The fence is ordinary Python, so the hard part is keeping the register review separate from the prose pass. Disclosure: This article was prepared as part of MonkeyCode's product outreach. A free model on MonkeyCode can draft the explanation layer, while the free server runs the checker on a clean checkout. Reviewers then share one command, instead of depending on a laptop that happens to hold the script.
Those two options are availability claims only; this page does not state model names, quotas, hardware, or how long either option remains. If the hosted run is inconvenient, the identical script still belongs in the repository and should pass on a local interpreter. If a claim register already exists, that hosted pair is enough to try this split without new local setup.
Limitations that should stay visible
The checker does not understand meaning, and a cited sentence can still distort the claim it names. A human still has to read each cited sentence against its statement and its source path. Pattern lists miss soft inventions such as typical teams or unnamed integrations, so owners should extend the suspect list for their domain. The sample date, paths, and statements are illustrations, not observations taken from a production documentation program.
Heading locks do not stop a model from smuggling a procedure into an ordinary explanatory paragraph. If the page needs commands, point to the owned command matrix instead of expanding this register. Retired claims must be removed from drafts in the same change that retires them, or the unused-claim rule will not match the reader's history. Pages that carry legal, security, or billing language should not rely on this fence as their only control.
Who should skip this split
Skip the workflow when you do not yet have a human who can own the register and open the source paths. A model cannot appoint that owner, and a green checker cannot replace the human review that is missing. Skip it for API references that already bind prose to extracted signatures, because a second register would drift from the generator. Skip it when the page is a narrative without factual claims, since the fence would only add tokens that imply false precision.
Use the split when concept pages are copied into support replies and release notes, and when those copies keep inventing behavior between reviews. The register then becomes the shared citation layer, and the model stays a drafter of connections rather than a second architect. Keep the checker in version control, and review its patterns the same way you review any other test. Widen those patterns only after a false acceptance that you can point to in a diff.
Top comments (0)