A tutorial stays reviewable when every generated sentence is traceable to a command transcript, and every risky claim stays in a human-owned field. Models can turn recorded commands into readable steps, but they should not invent versions, durations, or safety rules. This workflow keeps those jobs apart with a small ledger, a local checker, and a narrow drafting pass. The checker is a proposal you can run locally; it is not a measured production benchmark.
Why transcript-led tutorials fail in review
Most tutorial defects are not grammar problems, because reviewers argue about claims that no command in the transcript actually proved. A draft might state a supported Python version, a typical runtime, or a safe default that the author never recorded. Those sentences look helpful in review, and they also become false the moment the toolchain or the lockfile moves. A written ledger makes that split explicit before any model is allowed to rewrite the page.
The split is narrower than a full docs compiler, and it does not replace API references or error catalogs. It applies only to getting-started pages whose steps come from a saved terminal session with exit codes. Narrative may explain why a command exists, while ownership stays with the person who will sign the page. If the transcript lacks exit codes or working directories, the workflow should stop rather than guess missing context.
What each role may change
Decision table
| Ledger class | Source of truth | Model may draft | Human must own |
|---|---|---|---|
| command | transcript line | plain-language restatement | exact argv and working directory |
| observation | recorded exit code or stdout hash | short description of the observation | whether that observation is still required |
| narrative | none beyond the transcript | transitions and purpose sentences | deletion of any invented claim |
| pin | release notes or lockfile, not the model | nothing | version, date, and compatibility sentence |
| hazard | security or operations review | nothing | secrets, data loss, and auth warnings |
| support | product or team policy | nothing | who answers, and what is not covered |
The table is a decision rule, not a study result, and it should be edited when your review policy changes. A model may draft only the narrative class, and only from fields the checker has already marked draftable. Humans own pins, hazards, and support copy because those sentences create obligations the transcript itself cannot prove. Observations can be described by a model, but a human still decides whether the check remains a release gate.
Step 1: Record a transcript without prose
Save commands, exit codes, and optional stdout hashes in YAML, and leave explanatory sentences out of the file. Hashes stay optional because some command outputs contain timestamps or ids that cannot be matched exactly. The sample below is illustrative data for a local docs tool, and it is not a report from a hosted run.
page_id: getting-started
signer: required
transcript:
- id: t1
argv: ["python", "-m", "pip", "install", "-e", ".[docs]"]
cwd: repo-root
exit_code: 0
- id: t2
argv: ["python", "docs_ledger.py", "transcript.yaml"]
cwd: repo-root
exit_code: 0
owned:
pins:
- id: py
text: "This page was checked with the Python version named in .python-version."
hazards:
- id: secrets
text: "Do not paste tokens into the transcript file or the model prompt."
support:
- id: scope
text: "This walkthrough is not a support commitment for your production deploy."
draftable:
- id: n1
from: [t1, t2]
intent: "Explain that install precedes the ledger check."
Keep the YAML in review, because the prose is disposable and the transcript is the evidence. Do not store secrets, customer names, or internal hostnames in argv fields, notes, or later prompts. If a command prints a credential or a session token, redact the capture before anyone drafts a page.
Step 2: Reject owned claims that leak into drafts
The checker fails on empty owned sections, banned phrases in draftable intents, and transcript ids that do not exist. It also fails when the signer field is anything other than required, since an unsigned page is not finished. The script below is a local proposal you can adapt, and it does not call a network or a model. Install PyYAML yourself, and treat the snippet as unexecuted example code until you run it on your file.
import sys
import yaml
OWNED_KEYS = ("pins", "hazards", "support")
BANNED_IN_DRAFT = (
"supported until",
"usually takes",
"guaranteed",
"production-ready",
)
def main(path: str) -> int:
with open(path, encoding="utf-8") as handle:
doc = yaml.safe_load(handle) or {}
if not isinstance(doc, dict):
print("ledger root must be a mapping")
return 1
errors = []
transcript = doc.get("transcript") or []
if not transcript:
errors.append("transcript is empty")
for row in transcript:
if "exit_code" not in row or "argv" not in row:
errors.append(
f"transcript row missing argv or exit_code: {row.get('id')}"
)
owned = doc.get("owned") or {}
for key in OWNED_KEYS:
rows = owned.get(key) or []
if not rows:
errors.append(f"missing human-owned section: {key}")
for row in rows:
if not str(row.get("text", "")).strip():
errors.append(f"empty owned text in {key}")
known_ids = {row.get("id") for row in transcript}
for row in doc.get("draftable") or []:
intent = str(row.get("intent", "")).lower()
if any(phrase in intent for phrase in BANNED_IN_DRAFT):
errors.append(
f"draftable intent looks like an owned claim: {row.get('id')}"
)
sources = row.get("from") or []
if not sources:
errors.append(f"draftable row lacks transcript ids: {row.get('id')}")
missing = [item for item in sources if item not in known_ids]
if missing:
errors.append(f"unknown transcript ids: {missing}")
if doc.get("signer") != "required":
errors.append("signer must stay required")
if errors:
print("\n".join(errors))
return 1
print("ledger ok: narrative remains draftable, owned sections exist")
return 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: python docs_ledger.py transcript.yaml")
sys.exit(2)
sys.exit(main(sys.argv[1]))
python -m pip install pyyaml
python docs_ledger.py transcript.yaml
Run the checker before you ask a model for prose, so a bad ledger never becomes a prompt. A passing run only proves the file shape, and it does not prove the commands still succeed on a later day. Re-run the recorded argv when the lockfile or the toolchain changes, then update observations under human review. Treat a green check as a necessary gate, not as evidence that the tutorial is correct.
Step 3: Draft only the narrative lane
Build the prompt from draftable intents and the matching transcript ids, and omit the owned text entirely. The omission matters, because models often echo nearby warnings and then soften them into false certainty. Ask for transitions between recorded commands, and ask for no version numbers, timings, or security advice. Compare the returned sentences against the banned-phrase list before you paste them under the published steps.
Draft narrative only for page getting-started.
Use transcript ids t1 and t2, and explain order plus purpose.
Do not state versions, durations, guarantees, or security advice.
Do not edit owned pins, hazards, or support text.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Send the narrative prompt with free model access, and use the free server option when a local client is not the path you want. Those two availability facts are the only product claims used here, and they are not a quota, uptime, or quality promise. If you skip the hosted path, the same prompt still belongs in any editor that can read the YAML.
Paste the model output into drafts/n1.md, then run a second check that the file does not contain owned phrases. A simple search is enough for the first gate, and a reviewer still reads every sentence. The checker script does not open the draft file, so this search is a separate and required gate. Do not let the model edit owned blocks in the YAML, even when it offers a cleaner warning.
grep -n -E -i "guaranteed|usually takes|supported until|token|password" drafts/n1.md
If the search hits, delete the draft and tighten the prompt instead of hand-waving the match. The human signer then writes or confirms the pin, hazard, and support blocks in the same pull request. The page is not publishable until that signer is named in the review, because the checker cannot know who accepted the risk. Leave the model output in the pull request as a suggestion, and require the signer to accept or replace it.
Step 4: Assemble the page in a fixed order
Render commands from the transcript, then narrative from the accepted draft, then owned blocks without rewriting them. A fixed render order stops a later edit from burying a hazard under a friendly introduction. Suggested section order is human-owned prerequisites, commands from the transcript, narrative between commands, and a closing support note. Keep generated sentences in a review comment if your team wants the main diff to stay small.
Number the published steps to match transcript ids, so a failed command maps back to one ledger row. Do not renumber for style after review starts, because comments will point at the old ids. When a command is removed from the transcript, remove its narrative rather than leaving an orphan explanation. That rule keeps the page shorter, and it also prevents advice for a step that no longer exists.
The helper below only returns section ids, and it does not write Markdown or call a model. Use it in a unit test to lock that order before anyone edits the prose by hand. Label the helper as a proposal, because it has not been run as a publishing integration.
def render_order(doc: dict) -> list[str]:
blocks = ["prerequisites"]
for row in doc.get("transcript") or []:
blocks.append(f"command:{row.get('id')}")
blocks.append(f"narrative:{row.get('id')}")
blocks.append("support")
return blocks
Limits and who should skip this
This ledger does not fit legal terms, incident reports, billing pages, or any document whose wording is itself the contract. It also fails when the only source is a chat log, because chat is not a transcript with exit codes. Teams without a named signer should not publish the generated page, even if the checker exits zero. Regulated procedures should keep their existing control documents and should not route those sentences through a draft model.
The banned-phrase list is incomplete by design, and a careful model can still imply a promise without using those words. Human review is the control, and the script only catches structural mistakes and a few obvious leaks. A hosted or local drafting pass does not make the draft authoritative, and it does not extend a support window. Re-record the transcript when versions move, rather than asking a model to refresh the page from memory.
If a signer and a clean transcript are ready, run one narrative pass with free model access or the free server option. Keep the owned blocks in that same review, and publish the page only after the signer accepts them. The useful result is a tutorial whose friendly lines cannot outrun the commands you actually recorded. That review habit matters more than the particular editor or server that sent the narrative prompt.
Top comments (0)