Source profileQuality 93/100Review permissions

terrylica/cc-skills/plugins/doc-tools/skills/teachback/SKILL.md

teachback

Build or deepen a single-file interactive HTML explainer that teaches back difficult material already established in the conversation (or a named subject/source). Use when someone wants a durable, self-contained lesson with rendered mathematics, an exploratory multiple-choice assessment, and a terminology loop that lets the reader push unfamiliar terms back for deeper coverage.

Source repository stars
61
Declared platforms
0
Static risk flags
4
Last source update
2026-08-26
Source checked
2026-08-28

Decision brief

What it does: where it fits

Self-Evolving Skill: this file improves through use. If an instruction here is wrong, a gate rejected a page it should have accepted, or you needed a workaround — fix this file immediately, don't defer. Only for real, reproducible issues. And when you add a check to a gate, add…

Best for

  • Use when someone wants a durable, self-contained lesson with rendered mathematics, an exploratory multiple-choice assessment, and a terminology loop that lets the reader push unfamiliar terms back for deeper coverage.

Not for

  • Tasks that require unconfirmed production actions or broad system permissions.
  • Environments where the pinned source and install steps cannot be inspected.

Compatibility matrix

Platform support, with evidence labels

PlatformStatusEvidenceWhat to check
CodexNot declaredNo explicit evidencePortability before use
Claude CodeNot declaredNo explicit evidencePortability before use
CursorNot declaredNo explicit evidencePortability before use
Gemini CLINot declaredNo explicit evidencePortability before use
Open the compatibility checker

Installation

Inspect first. Install second.

The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

Source-detected install commandSource
npx skills add https://github.com/terrylica/cc-skills --skill "plugins/doc-tools/skills/teachback"
Safe inspection promptEditorial

Inspect the Agent Skill "teachback" from https://github.com/terrylica/cc-skills/blob/05f53c5b24a445c1895e9b0590212e66cd70f39e/plugins/doc-tools/skills/teachback/SKILL.md at commit 05f53c5b24a445c1895e9b0590212e66cd70f39e. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

Workflow

What the source asks the agent to do

  1. 01

    Assessment — exploratory, not an examination

    3–8 questions covering the major objectives, including definition/boundary recognition, mechanism or reasoning, and transfer to a new closely related case.

    Selecting the correct answer marks it correct — and every other option stays selectable.After the answer is found, clicking a distractor shows what that option assumes and why it isBefore the answer is found, an incorrect selection names the misconception and gives a targeted
  2. 02

    Emit the assessment as JSON, not as a JavaScript literal

    The questions live in a block inside , and the page's own script does JSON.parse(...) on that block. One source of truth: the gate reads exactly what the page reads.

    The questions live in a block inside , and the page's own script does JSON.parse(...) on that block. One source of truth: the gate reads exactly what the page reads.This is structural, not stylistic. A per-question contract cannot be soundly verified by regexing a JS object literal, and the attempt failed in the favourable direction: counting document-wide ok: true occurrences cert…Placing the block inside also fixes the ledger: question text is teaching content, but in a after it sat outside the revision hash, so editing a question changed nothing in the revision id.
  3. 03

    Verification

    Three tools, all Python. PEP 723 inline metadata makes each file self-contained: uv resolves each gate's dependency into an ephemeral environment, so nothing has to be installed first and the gates run against any project, in any language, on a machine with only uv present.

    Three tools, all Python. PEP 723 inline metadata makes each file self-contained: uv resolves each gate's dependency into an ephemeral environment, so nothing has to be installed first and the gates run against any proje…Resolve the skill directory first, then invoke with --no-project --script:The last line takes no page: it is the regression guard for the coverage audit below, and like the gates it carries its own PEP 723 metadata, so it resolves pytest for itself.
  4. 04

    Six rules that generalise every gate defect found in review

    Eight review rounds and an adversarial self-review found the same few confusions repeatedly. They are worth stating as rules, because each one closes a class that instance patches only delay.

    Eight review rounds and an adversarial self-review found the same few confusions repeatedly. They are worth stating as rules, because each one closes a class that instance patches only delay.1. Presence is not content. selectone(".reads") is not None is satisfied by , and .chips .chip by . A generator that emits the right skeleton with nothing in it was certified complete. Every annotation check therefore a…2. A source pattern is not a rendered property. Scanning for \class{sym- proves the markup was written; it does not prove the reader sees colour. One CSS rule — color: inherit !important — makes every symbol black while…
  5. 05

    Options

    Every option is optional. /teachback with no arguments is the common case.

    Every option is optional. /teachback with no arguments is the common case.Composition: --terms, --revise and --update operate on an existing page and always append a revision-ledger entry. Everything else builds a new page.

Permission review

Static risk signals and limitations

Network access

medium · line 96

The documentation includes network, browsing, or remote request actions.

src="https://cdn.jsdelivr.net/npm/[email protected]/es5/tex-mml-chtml.js"

Network access

medium · line 350

The documentation includes network, browsing, or remote request actions.

content="default-src 'none'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline'; font-src https://cdn.jsdelivr.net; form-action 'none'; base-uri 'none'"

Runs scripts

medium · line 428

The documentation asks the agent to run terminal commands or scripts.

uv run --no-project --script "$(cc-plugin-root doc-tools)/skills/teachback/verify_explainer.py" <page.html> --revision

Runs scripts

medium · line 458

The documentation asks the agent to run terminal commands or scripts.

*Resolve the skill directory first, then invoke with `--no-project --script`:**

Reads files

low · line 601

The documentation asks the agent to read local files, directories, or repositories.

Resolve the file to an absolute path and open **that exact file** in the default browser

Writes files

medium · line 614

The documentation asks the agent to create, modify, or delete local files.

to work around an instruction above — update this file and the gate together, now, not "next time".

Evidence record

Why each signal appears

EvidenceSourceComputedTestedEditorial
SignalValueEvidence typeMeaning
Quality score93/100ComputedDocumentation, specificity, maintenance, and trust rules
Repository stars61SourceRepository attention, not individual Skill quality
Compatibility0 platformsSourceDeclared in the catalog source record
Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

Pinned source

Provenance and original SKILL.md

Repository
terrylica/cc-skills
Skill path
plugins/doc-tools/skills/teachback/SKILL.md
Commit
05f53c5b24a445c1895e9b0590212e66cd70f39e
License
MIT
Collected
2026-08-28
Default branch
main
View the original SKILL.md

teachback — teach the material back as a self-contained interactive page

Self-Evolving Skill: this file improves through use. If an instruction here is wrong, a gate rejected a page it should have accepted, or you needed a workaround — fix this file immediately, don't defer. Only for real, reproducible issues. And when you add a check to a gate, add the mutation that proves it can fail: an unexercised check is not yet a check.

Produces one HTML file that explains hard material to an intelligent reader with no assumed specialist background. The page runs from file:// with no server, no tracking and no framework.

The defining behaviour: teachback reads what is already in the working context. Do not ask the reader to restate material the conversation already contains. Infer the subject, scope, prerequisites, title, filename and technical depth.

This copy is canonical. teachback was built in Eon-Labs/alpha-forge and merged there as #527 after eleven review rounds; it was promoted to terrylica/cc-skills on 2026-08-20 to make it available in every project rather than one repository. Edit it here. The alpha-forge path .claude/skills/teachback/ is being reduced to a redirect stub carrying no gate scripts; until that lands, two full copies exist and only this one is authoritative. If you find yourself editing the alpha-forge copy, stop and edit this one.


Options

Every option is optional. /teachback with no arguments is the common case.

OptionEffect
(none)Explain the most recent coherent discussion in the working context.
<subject>Explain a named subject instead, e.g. /teachback the spanning admission result.
--source <path|issue|url>Explain designated material rather than the conversation: a file, #123, a doc path, a URL.
--out <path>Write the page here. Default: ~/Documents/explainers/<slug>.html, outside the repository — generated pages are artefacts, not source. Pass an in-repo path only when the page is meant to be committed.
--update <path>Revise an existing page in place instead of creating one. Implied by --terms and --revise.
--terms "a, b, c"The terminology loop. Add or substantially deepen coverage of these terms in an existing page. This is what the page's own footer form emits.
--revise "<instruction>"Any other change to an existing page, in plain language.
--depth intro|standard|deepHow much advanced qualification to carry. Default standard. deep keeps every limitation, objection and open question.
--questions <n>Assessment size. Default 3–8, chosen to cover the objectives.
--open / --no-openLaunch the default browser on completion. Default --open.
--dark / --lightForce a colour scheme. Default: follow the operating system.

Composition: --terms, --revise and --update operate on an existing page and always append a revision-ledger entry. Everything else builds a new page.


What the page must contain

Include a section only when it materially improves understanding. Do not pad to fill a structure.

  1. Orientation and explicit learning objectives
  2. A concise 101-level account
  3. Prerequisites and terminology
  4. Canonical definitions
  5. Components, variables, actors, stages or layers
  6. The literal mechanism or logical sequence
  7. Conditions, boundaries, exclusions, common confusions
  8. A real example, a non-example, and a near-miss
  9. A justified worked application
  10. Advanced qualifications, limitations, exceptions, open questions
  11. The diagnostic assessment
  12. The terminology request form and the revision ledger

Provenance marking is mandatory

Distinguish three kinds of statement, visibly, with the .prov spans:

  • SOURCE-ESTABLISHED — measured or stated in the underlying material
  • INFERENCE — explanatory reasoning added by the explainer
  • OPEN — unresolved, contradictory, or missing evidence

Mark contradictions and uncertainty; never invent a resolution. Where a later correction supersedes an earlier statement in the source material, the correction wins and the supersession is stated.

Pedagogy constraints

Reduce prerequisite burden and linguistic complexity without reducing technical precision. Preserve accepted terminology, definitions, mechanisms, causal relationships, boundary conditions and qualifications.

Do not use analogies, metaphors, fictional comparisons, or approximate substitutions that alter the concept. Explain the thing itself.


Mathematics

Render with MathJax 3, from a version-pinned CDN URL carrying a subresource-integrity hash:

<script
  id="mj"
  async
  src="https://cdn.jsdelivr.net/npm/[email protected]/es5/tex-mml-chtml.js"
  integrity="sha384-Wuix6BuhrWbjDBs24bXrjf4ZQ5aFeFWBuKkFekO2t8xFU0iNaLQfp2K6/1Nxveei"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

Never point the primary source at a local absolute path. An earlier version of this skill prescribed file:///Users/<name>/node_modules/mathjax/... with a JavaScript error fallback to the CDN. Measured, that fallback does fire and the page does render — but the pattern is wrong twice over: the path cannot travel with the artifact, and served over http(s) a browser refuses file:// subresources outright, logging Not allowed to load local resource and leaving the generating machine's home directory in the DOM for anyone who receives the file.

integrity pins the bytes, not merely the URL: a swapped or compromised CDN object fails to execute rather than running silently. Recompute it if the pinned version changes — curl -sSL <url> | openssl dgst -sha384 -binary | openssl base64 -A.

If a page must render with no network at all, pre-render the mathematics to SVG at generation time rather than adding a local script source; SVG output inlines glyph paths, so it needs no font requests. Do not add a file: source to a page that will be shared.

  • Preserve every material equation, variable, operator, constraint, domain, assumption and subscript.
  • Give consequential equations stable numeric identifiers and keep them stable across revisions.
  • Put the raw LaTeX in a <details> panel beneath each numbered equation so it is recoverable.
  • Define symbols, units, assumptions and validity conditions near first use.
  • Explain what each equation asserts and how its terms relate; show consequential intermediate steps.
  • Distinguish source equations from added derivations.
  • If an expression looks wrong or ambiguous, preserve it and add a marked note — never silently correct or simplify it away.
  • Never render mathematics as images, canvas, or Unicode approximations.

Rendering the equation correctly is the floor, not the goal. Every equation must additionally satisfy Notation accessibility below.


Notation accessibility (release-blocking)

The reader is assumed to be symbolically illiterate but not unintelligent: fluent in the subject matter, unpractised at decoding notation. An equation printed bare forces them to hold a symbol in memory, hunt for its definition in surrounding prose, carry the meaning back, and repeat — the split-attention effect, and it is the single largest avoidable cost on a page like this.

Every numbered equation carries four things. Fewer is a defect, not a style choice.

1. Systematic colour encoding

Colour symbols by the kind of thing they are, with one fixed vocabulary reused across the whole page. The same colour must mean the same role in every equation; a symbol must never change colour.

RoleMeaningTypical symbols
sym-outthe quantity the equation exists to defineΔ, α, IR
sym-pospositions, weights, loadings — what is heldw, w⋆, β
sym-retreturns; what an instrument or book earnedr, g, y, e
sym-timetime indices, counts, block labelst, i, T
sym-costcosts, turnover, penaltiesc, κ, λ
sym-dispdispersion, covariance, risk aversionσ, Σ, γ

Adapt the roles to the subject; keep the discipline. Six is near the ceiling — beyond that the key itself becomes the thing to memorise.

Implementation is fixed, because the obvious version silently fails:

// Listing the package is NOT enough. tex-mml-chtml does not bundle the html extension, so without
// this loader entry \class renders as literal red "\class" text in the output.
loader: { load: ['[tex]/html'] },
tex: { packages: { '[+]': ['html'] } }

Then $\class{sym-out}{\Delta} = \class{sym-pos}{w}\,\class{sym-ret}{r}$, with colour defined once per role as a CSS custom property. Use a colourblind-safe palette (Okabe–Ito) and give every role a distinct dark-mode value; a hue that reads well on white can vanish on near-black.

Two properties make the code trustworthy, and both are checked:

  • One role per symbol, everywhere. A symbol that is sym-out in one equation and sym-disp in another teaches the reader that the colour is arbitrary.
  • The code extends into prose. A symbol mentioned in a paragraph carries the colour it was given in the equation that defined it. An encoding applied only inside the equation boxes reads as decoration. Colour role-bearing symbols only — a bare statistic such as p = 0.027 has no role, and colouring it dilutes the vocabulary.

Verify by inspecting the emitted DOM, never by assuming. Count mjx-container .sym-* occurrences and read back getComputedStyle().color in both schemes. A silent \class failure looks like ordinary red text and is easy to scroll past.

2. Colour is redundant, never load-bearing

Every coloured symbol also carries a chip beneath its equation: the symbol, what it means in ordinary words, and its shape or value. Nothing on the page may require distinguishing two hues.

Chips sit directly under the equation they annotate — spatial contiguity. A legend elsewhere on the page reintroduces exactly the hunting the colours were meant to remove.

Every chip declares which symbols it explains, in data-symbol, verbatim as they appear inside \class{…}{…}, separated by |:

<span class="chip sym-time" data-symbol="t|t-1"
  ><b>t, t−1</b><span>the bar being scored, and the bar before it</span
  ><i
    >the weight is fixed at t−1, so it cannot use anything observed at t</i
  ></span
>

Declared rather than inferred, because the chip renders μ_p as glyphs while the source says \mu_{p}; matching them would be a normalisation guess, and a guess that fails reads as coverage. The gate requires every (role, symbol) pair an equation colours to be declared by one of its chips. Comparing role sets is not enough — an equation colouring both g and n as sym-out with one chip has identical sets on each side, which is how t, t−1, g and R stayed coloured and unnamed in the reference page through several rounds of review.

3. A notation key before the first equation (pre-training)

Introduce the role vocabulary and the typographic conventions — bold means a collection, a subscript is a label and not a multiplication, a star means "chosen to be best" — before the first equation, never inline with it. Learning six categories once is cheaper than decoding forty symbols later.

The key must survive the expert switch below: it is the only place the encoding is decoded, so hiding it strands the reader with an undocumented code.

4. Equation as a sentence, then as numbers

Each equation gets:

  • a Reads as block — the equation restated as one plain-language sentence, subject and verb, no symbols. Not a gloss of the notation; a statement of what it asserts.
  • a With numbers block — the equation instantiated with the actual figures from the material, including what the result was and why. Where a real figure is unavailable, say so; never invent one.

Both blocks contain mathematics, and mathematics cannot line-break. Give .eq, .reads and .numbers overflow-x:auto so a long expression scrolls inside its own block. Omitting this was measured at 120px of page-level horizontal scroll on a 390px viewport, caused by one inline arithmetic line — a local formatting choice turning into a whole-document defect.

Use \underbrace{...}_{\text{...}} to name multi-term groups inside the equation. This is signalling: it partitions a wall of symbols into two or three named chunks that can be read independently, and it is the one annotation the reader cannot fail to associate with the right terms.

5. Expertise reversal — all of it must be switchable

The same annotation that measurably helps a first reading measurably hinders a reader who has internalised the notation, because reconciling redundant explanation is itself work. Provide one page-level control that hides the readings, chips and worked numbers; keep the colour encoding and the notation key, since neither costs vertical space. Persist the choice in localStorage inside a try/catch — private-browsing mode must degrade to "not remembered", not to a thrown error.

6. Do not claim accessibility features you have not verified

Recorded negative, so it is not re-attempted blind: the MathJax a11y explorer is not usable in a portable single file. Measured on 2026-08-18 —

  1. loader.load: ['[a11y]/explorer'] resolves the unregistered [a11y] prefix against the document, 404s, and aborts MathJax startup: container count 8 → 0. An optional extra took every equation.
  2. Loading it after startup with the path repaired throws EnrichedMathItemMixin, because the bundle registers no dependency metadata for [a11y] either.
  3. Loading the whole chain in order (sre, semantic-enrich, complexity, explorer) succeeds and sets explorer = true, but no aria-label and no data-semantic-speech is ever attached, even after a forced rerender(). The module is present and inert.

Hard-coding the a11y path at config time works locally, but the artifact is meant to travel and a stale absolute path then takes the equations down with it. The Reads as sentence is the substitute, and it is purpose-written prose rather than machine-generated speech. If a future revision does enable speech, gate the on-page claim on the capability actually being present, not on the module loading.


Layout

  • Page shell occupies 92–100% of desktop width; no globally narrow container.
  • Body text fills the content column. Operator decision 2026-08-18: a 65–80ch cap beside a wide shell leaves a large void to the right of every paragraph, which reads as hard-wrapped text. Prose, cards, tables, equations and the assessment all use the same width. Do not reintroduce a prose cap or a marginalia rail without being asked.
  • Contents is a collapsed sticky <details> disclosure, not a sidebar rail. Operator decision 2026-08-18: a permanent rail spends horizontal space on navigation that is consulted occasionally. Closed it costs one ~40px bar; open it expands to a multi-column list. Native <details> keeps it keyboard-accessible and working with JavaScript disabled.
  • Do not make .shell a grid. If it ever becomes one, every direct child (header, nav, main, footer) needs an explicit grid-column. A child without one auto-flows into the next free cell — which is column 1 — and that is how the revision ledger once rendered inside the narrow contents column.

These are RENDERED invariants, and only verify_rendered.py may assert them. Do not add a layout check to the static gate. Whitespace and declaration order are insignificant in CSS, so matching source text is unsound in principle, and it was unsound in practice: the pattern .shell{display:grid matched, while the exactly equivalent .shell { display: grid; } bypassed the grid-child check entirely — the check silently enforced nothing on any page whose CSS happened to be formatted (Eon-Labs/alpha-forge#527 review, finding 4). The rendered gate reads these back from getComputedStyle and from measured geometry, where formatting cannot change the answer.

  • Keyboard navigation, visible focus, sufficient contrast, dark mode, print, and full intelligibility with JavaScript disabled (JS is required only for assessment validation and the terminology form).
  • Keep all explanatory content in the DOM. No decorative clutter, gratuitous animation, oversized hero, marketing language, or dashboard elements.

Assessment — exploratory, not an examination

3–8 questions covering the major objectives, including definition/boundary recognition, mechanism or reasoning, and transfer to a new closely related case.

One best answer, three purposeful distractors. Every distractor encodes a different plausible misconception arising from the material. No filler, trivia, deceptive wording, test-taking tricks, "all of the above" or "none of the above".

The reader is never locked out. This is the point.

  • Selecting the correct answer marks it correct — and every other option stays selectable.
  • After the answer is found, clicking a distractor shows what that option assumes and why it is wrong, presented as exploration rather than failure.
  • Before the answer is found, an incorrect selection names the misconception and gives a targeted hint without revealing which option is correct, and another attempt is permitted.
  • Per-question reset, reset-all, and a live summary of objectives demonstrated versus needing review.
  • Track and report misconceptions explored, not just correctness. A reader who has read every distractor's explanation has learned more than one who guessed right first time.
  • Never transmit, store or track responses. No localStorage, no fetch, no cookies.

Emit the assessment as JSON, not as a JavaScript literal

The questions live in a <script type="application/json" id="assessment-data"> block inside <main>, and the page's own script does JSON.parse(...) on that block. One source of truth: the gate reads exactly what the page reads.

This is structural, not stylistic. A per-question contract cannot be soundly verified by regexing a JS object literal, and the attempt failed in the favourable direction: counting document-wide ok: true occurrences certified a page where one question had two correct answers and another had none, because the totals matched (Eon-Labs/alpha-forge#527 review, finding 1). Parsing makes "one correct and three distractors in every question" checkable, and makes duplicate question ids detectable.

Placing the block inside <main> also fixes the ledger: question text is teaching content, but in a <script> after </main> it sat outside the revision hash, so editing a question changed nothing in the revision id.


Privacy: deny by default with a Content Security Policy (release-blocking)

The page is generated from private working context. An exfiltration channel is therefore a real risk, not a theoretical one, and a blacklist of JavaScript APIs cannot close it: a remote <img>, a <script>, a <link>, a <form action>, a WebSocket and an EventSource are all requests, and none of them calls fetch (Eon-Labs/alpha-forge#527 review, finding 7).

Every page carries this as the first element after <meta charset>, before any content it must govern — a policy in <meta> does not apply to markup the parser has already seen:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'none'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline'; font-src https://cdn.jsdelivr.net; form-action 'none'; base-uri 'none'"
/>

connect-src, img-src and frame-src are deliberately absent: they fall back to default-src 'none'. 'unsafe-inline' is required because the page's own <style> and <script> are inline and MathJax injects styles at runtime; it is not a weakness here, because default-src 'none' still denies every destination.

Measured, not assumed. Against a local sink on a separate origin, a page without this policy delivered five requests — image beacon, remote script, remote stylesheet, WebSocket handshake, SSE stream. With it, zero arrived, and all equations still rendered. Re-run that comparison if the policy changes; a CSP that blocks nothing is not a control.

Three properties of that policy are contractual, not incidental:

  • It precedes every element that can issue a request. A policy in <meta> does not govern markup the parser has already seen, so a late policy leaves everything above it ungoverned — including a beacon on the allowed origin, which both gates would otherwise wave through.
  • The single remote reference is an exact URL, not an origin. An origin allowlist is not enough: any path on that origin can carry page content in its query string.
  • That reference carries the expected integrity value, not merely some integrity attribute.

localStorage is permitted for exactly one key, teachback:annotations, holding the annotation preference. Reader responses are never stored. Every access sits inside its own try/catch, so private-browsing mode degrades to "not remembered" rather than throwing.

Enumerate storage references, then subtract the permitted ones — never the reverse. Scanning for localStorage.setItem( is an allowlist with an open back door: localStorage["setItem"](…) is ordinary JavaScript that produced zero detected accesses and zero unguarded offsets, so a page persisting reader answers passed both storage checks at once.

Then stop arguing about syntax and observe the storage. The static rules ban computed global access and runtime-built property keys, but that is a constraint on shape — it closes the spellings someone thought of, and document.defaultView, Reflect.get(document, "defaultView") and whatever comes next are not among them. verify_rendered.py therefore exercises the page — every question answered, the annotation toggle flipped — and then reads Object.keys(localStorage), asserting the set is exactly {teachback:annotations}, with sessionStorage and document.cookie empty.

Keep both. The static rules fail fast and say why at the point of authorship; the observation is the one that actually holds, because it does not care how the code was written.


Terminology loop

The page ends with a form where the reader lists terms they did not understand.

The form cannot call a model; it composes a ready-to-paste instruction and copies it to the clipboard, naming the page path and current revision id:

/teachback --update <abs-path> --terms "portfolio admission, certainty equivalent"

When invoked with --terms, expand those terms substantively in the terminology section — a real definition, why it matters here, and how it differs from the nearest thing it is confused with. A one-line gloss is not sufficient. Add a <dfn>-style anchor so the term can be linked from the body.


Revision ledger

Every page carries a ledger as the last element before the closing footer text. Each update appends one row; earlier rows are never rewritten.

FieldMeaning
#Monotonic revision number, starting at 1
UTC timestampWhen the revision was produced
Revision idFirst 12 hex of sha256 over the exact bytes from <main id="main"> to </main> inclusive
ChangeOne line describing what this revision altered
CommitThe git commit that carried it, when the page lives in a repository; otherwise

Hashing <main> only is deliberate: the ledger lives in the <footer>, so recording a revision id cannot change the value being recorded. Recompute and verify with:

uv run --no-project --script "$(cc-plugin-root doc-tools)/skills/teachback/verify_explainer.py" <page.html> --revision

Zero hard-wrapping invariant (release-blocking)

Every <p> element must occupy exactly one physical line in the saved source. Browsers soft-wrap responsively; the source must contain no newline, <br>, <wbr> or newline entity between <p...> and </p>. Do not use white-space: nowrap.

Apply the check after every generation, formatting or build step, and make it the last file-content operation before opening the browser.

The naive whole-document regex <p\b[^>]*>[\s\S]*?[\r\n][\s\S]*?</p> is unsatisfiable and must not be used as the gate. Being non-greedy across the whole document, it opens in one <p>, crosses that element's </p> and the following newline, and closes at a later </p> — so it fires on any document containing two well-formed adjacent paragraphs on separate source lines. The correct test pairs each <p ...> with its own nearest </p> and requires that body to be newline-free. verify_explainer.py implements the correct test and reports the naive count separately so the distinction stays visible.


Verification

Three tools, all Python. PEP 723 inline metadata makes each file self-contained: uv resolves each gate's dependency into an ephemeral environment, so nothing has to be installed first and the gates run against any project, in any language, on a machine with only uv present.

Resolve the skill directory first, then invoke with --no-project --script:

TB="$(cc-plugin-root doc-tools)/skills/teachback"

uv run --no-project --script "$TB/verify_explainer.py" <page.html>             # static gate
uv run --no-project --script "$TB/verify_explainer.py" <page.html> --revision  # print the revision id
uv run --no-project --script "$TB/verify_rendered.py"  <page.html>             # rendered gate (headless browser)
uv run --no-project --script "$TB/mutate_gate.py"      <page.html>             # negative fixtures
uv run --no-project --script "$TB/tests/test_check_discovery.py"               # the gates' own tests

The last line takes no page: it is the regression guard for the coverage audit below, and like the gates it carries its own PEP 723 metadata, so it resolves pytest for itself.

cc-plugin-root doc-tools prints the live install path from ~/.claude/plugins/installed_plugins.json. The CLAUDE_PLUGIN_ROOT placeholder is not a shell variable — Claude Code substitutes it only inside plugin manifests and injects it only into hook and MCP subprocesses, never into the Bash tool. Spelled in a skill body it reaches the shell unset, expands to empty, and yields an absolute-looking /skills/teachback/... that reads like a missing file instead of a missing variable. Globbing the version cache is not the fallback either: that directory retains orphaned versions and the highest semver is routinely not the live one.

--no-project states the isolation the gate depends on, rather than relying on it. It is not currently load-bearing, and this paragraph says so deliberately — the first draft of it claimed that plain uv run adopts the surrounding workspace and that the gates therefore had to be run from $HOME. That claim was inferred from a DEBUG Found workspace root: line, which reports workspace discovery, not workspace adoption, and it is false. Measured on uv 0.11.28 from a directory whose pyproject.toml declares an unresolvable dependency — a fixture first checked to be genuinely hostile, by confirming a plain uv run python -c ... there fails to resolve — all three of uv run <gate>, uv run --script <gate> and uv run --no-project --script <gate> reached the gate's own argparse. uv says as much itself: --no-project is a no-op for Python scripts with inline metadata; ignoring.

Keep the flag anyway. It costs nothing, it is correct for any invocation that is not a PEP 723 script (uv run pytest ... inside a project genuinely does resolve that project), and it keeps the documented command correct if a gate ever loses its inline metadata block. What it must not do is carry a mechanism story nobody re-measured.

What each one owns, and why the split is not arbitrary

ToolOwnsNever touches
verify_explainer.pysource-text and structural invariants: hard wrapping, per-equation annotations, per-question assessment shape, CSP, the ledgerlayout — see below
verify_rendered.pyanything only a browser can answer: computed colours, measured geometry, overflow, toggle behaviour, outbound requestssource text
mutate_gate.pyproving the static gate is not blindthe page itself (it restores it)

Layout lives exclusively in the rendered gate. Asserting a rendered property against CSS source text is unsound in principle and was unsound here: .shell{display:grid matched, .shell { display: grid; } did not, and the check enforced nothing on formatted CSS.

The static gate parses the page with BeautifulSoup rather than pattern-matching it, because the contracts are per-item. A document-wide count cannot see a per-item failure and fails in the favourable direction — extras on one equation mask a total absence on another. Where the gate must still look at text (the hard-wrap invariant, which is about whitespace, and the scripted-network check) it says so and scopes itself: the network check reads script bodies with comments stripped, after whole-file matching flagged a page's own prose sentence "none of them calls fetch()".

Every check must have been seen to fail

mutate_gate.py carries twelve mutations, one per defect class, and asserts the unmutated baseline is clean before counting any kill — a harness that always reports non-zero looks identical to one that works. Add a mutation whenever you add a check. A mutation that changes nothing, or cannot find its target, is reported as BROKEN rather than counted as a survivor: two mutations here first looked like blind gates and were in fact selecting a fragment with a non-greedy regex instead of the element with a parser.

A gate that cannot run is not a gate

verify_rendered.py bootstraps its own browser. PEP 723 resolves the Playwright package; it does not download the browser, so the documented command failed in a clean checkout with Executable doesn't exist — the same class of problem as the Bun dependency it replaced, one layer down. It now runs playwright install chromium once on first use rather than documenting a prerequisite nobody reads. Only a missing-browser failure triggers that; any other launch error surfaces.

Assertions must observe the state they are about

The lock-out check solves a question first, then clicks a distractor and confirms it still explains itself. Reading the initial disabled count proved nothing: code that disables the other options only after the correct answer is chosen is invisible before the question is answered.

The same principle killed the width-based grid check. Inferring "explicitly placed" from a child's width was vacuous in the common case — in a single-column grid every auto-flowed child is full width, so the check passed while nothing was placed — and wrong in the uncommon one, since a deliberately narrow but correctly placed child was reported as a fault. It now reads computed grid-column-start.

Six rules that generalise every gate defect found in review

Eight review rounds and an adversarial self-review found the same few confusions repeatedly. They are worth stating as rules, because each one closes a class that instance patches only delay.

1. Presence is not content. select_one(".reads") is not None is satisfied by <div class="reads"></div>, and .chips .chip by <span class="chip"></span>. A generator that emits the right skeleton with nothing in it was certified complete. Every annotation check therefore asserts a minimum of actual visible text, and absent collapses to the same answer as empty.

2. A source pattern is not a rendered property. Scanning for \class{sym- proves the markup was written; it does not prove the reader sees colour. One CSS rule — color: inherit !important — makes every symbol black while the string is still in the file. Colour is therefore read per equation from getComputedStyle in the rendered gate, and the static check is explicitly the weaker of the two.

3. A curated list is not enumeration. Watching localStorage, sessionStorage and document.cookie left IndexedDB, the Cache API and the Origin Private File System unexamined — the same losing game as naming Window aliases one at a time. Ask the browser what actually persists: indexedDB.databases(), caches.keys(), navigator.storage.getDirectory().

4. A minimum count is not coverage. "At least one non-empty chip" was satisfied by an equation using six colour roles and explaining one, and "at least three key chips" by a page using six. The contract is every role the equation actually uses is documented, so the check now compares the set of roles used against the set explained and names the difference. Enforcing this immediately found a real gap in the reference page: equation (6) coloured sym-time and never said what it was.

5. A kill is only evidence for the check it targets. A mutation that removed one sym-disp chip was reported KILLED — but equation (8) documents that role twice, so coverage never broke; the only failure was the revision id, which any edit changes. The mutation was a no-op for its own purpose and looked identical to a working one. So every mutation now declares the check it must trip, and a red run that misses it is reported MISDIRECTED and counted as a failure, not a kill.

6. An unexercised check is not yet a check. mutate_gate.py --audit-coverage lists every check no mutation targets. The first run said 56 of 71 — and every vacuous check found in review up to that point had been in that set, so that is where the rest were. Probing seven of them found six real defects, the worst being an ordering check that located elements with str.find and read the −1 "not found" sentinel as "earlier", so it passed for any ordering. Report the remaining count rather than "all mutations killed": a suite that says it is partial is honest.

The generalisation behind the first three: verify the property the contract is about, at the layer it lives on. Source text, DOM structure and rendered appearance are three different layers, and a check on the wrong one passes for the wrong reason. Rules 4 and 5 extend it one step further — a check can also read the right layer and still ask a weaker question than the contract, and a test suite can be green for a reason that has nothing to do with what it claims to prove.

Still do a human browser pass

The rendered gate does not cover print presentation, dark-mode legibility as a human judges it, or intelligibility with JavaScript disabled. Check those by looking.

Do not claim a check passed unless it was performed successfully.


Completion

Resolve the file to an absolute path and open that exact file in the default browser (open <abs-path> on macOS). Confirm the launch succeeded by querying the browser, not by trusting the launcher's exit code — open returns 0 whether or not the page loaded. If no graphical session is available, say so and give the exact manual command.

Then report: whether it opened, the path, one sentence on what it explains, and any unresolved source, mathematical, verification or environmental limitation.


Post-Execution Reflection

If a gate rejected a page that was in fact correct, accepted one that was in fact wrong, or you had to work around an instruction above — update this file and the gate together, now, not "next time". Two specific cases are worth the edit every time:

  • A check fired for the wrong reason. Record which layer the contract actually lives on (source text, DOM structure, rendered appearance) and move the check there. Most defects found in review were checks reading the wrong layer and passing for an unrelated reason.
  • A mutation was reported KILLED without tripping its own target. That is a MISDIRECTED result, not a kill; state which check the mutation is supposed to trip and fix the mutation, not the count.

When you add a check, add its mutation in the same edit, and re-run --audit-coverage so the remaining unexercised count stays honest rather than quietly growing.

Frequently asked questions

What to verify before installation and use

What does the teachback source document cover?

Self-Evolving Skill: this file improves through use. If an instruction here is wrong, a gate rejected a page it should have accepted, or you needed a workaround — fix this file immediately, don't defer. Only for real, reproducible issues. And when you add a check to a gate, add…

How do I install teachback?

The source record exposes this install command: npx skills add https://github.com/terrylica/cc-skills --skill "plugins/doc-tools/skills/teachback". Inspect the command and pinned source before running it.

Which permission-related actions were detected?

Static rules flagged network, exec-script, read-files, write-files in the source; the page lists the matching lines and excerpts.