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.
terrylica/cc-skills/plugins/doc-tools/skills/teachback/SKILL.md
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.
Decision brief
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…
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/terrylica/cc-skills --skill "plugins/doc-tools/skills/teachback"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
3–8 questions covering the major objectives, including definition/boundary recognition, mechanism or reasoning, and transfer to a new closely related case.
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.
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.
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.
Every option is optional. /teachback with no arguments is the common case.
Permission review
The documentation includes network, browsing, or remote request actions.
src="https://cdn.jsdelivr.net/npm/[email protected]/es5/tex-mml-chtml.js"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'"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> --revisionThe documentation asks the agent to run terminal commands or scripts.
*Resolve the skill directory first, then invoke with `--no-project --script`:**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 browserThe 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
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 93/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 61 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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-forgeand 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.
Every option is optional. /teachback with no arguments is the common case.
| Option | Effect |
|---|---|
| (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|deep | How 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-open | Launch the default browser on completion. Default --open. |
--dark / --light | Force 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.
Include a section only when it materially improves understanding. Do not pad to fill a structure.
Distinguish three kinds of statement, visibly, with the .prov spans:
SOURCE-ESTABLISHED — measured or stated in the underlying materialINFERENCE — explanatory reasoning added by the explainerOPEN — unresolved, contradictory, or missing evidenceMark 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.
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.
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.
<details> panel beneath each numbered equation so it is recoverable.Rendering the equation correctly is the floor, not the goal. Every equation must additionally satisfy Notation accessibility below.
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.
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.
| Role | Meaning | Typical symbols |
|---|---|---|
sym-out | the quantity the equation exists to define | Δ, α, IR |
sym-pos | positions, weights, loadings — what is held | w, w⋆, β |
sym-ret | returns; what an instrument or book earned | r, g, y, e |
sym-time | time indices, counts, block labels | t, i, T |
sym-cost | costs, turnover, penalties | c, κ, λ |
sym-disp | dispersion, 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:
sym-out in one equation and sym-disp in
another teaches the reader that the colour is arbitrary.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.
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.
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.
Each equation gets:
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.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.
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.
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 —
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.EnrichedMathItemMixin, because the bundle
registers no dependency metadata for [a11y] either.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.
<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..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.
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.
localStorage, no fetch, no cookies.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.
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:
<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.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.
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.
Every page carries a ledger as the last element before the closing footer text. Each update appends one row; earlier rows are never rewritten.
| Field | Meaning |
|---|---|
# | Monotonic revision number, starting at 1 |
| UTC timestamp | When the revision was produced |
| Revision id | First 12 hex of sha256 over the exact bytes from <main id="main"> to </main> inclusive |
| Change | One line describing what this revision altered |
| Commit | The 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
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.pyimplements the correct test and reports the naive count separately so the distinction stays visible.
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.
| Tool | Owns | Never touches |
|---|---|---|
verify_explainer.py | source-text and structural invariants: hard wrapping, per-equation annotations, per-question assessment shape, CSP, the ledger | layout — see below |
verify_rendered.py | anything only a browser can answer: computed colours, measured geometry, overflow, toggle behaviour, outbound requests | source text |
mutate_gate.py | proving the static gate is not blind | the 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()".
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.
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.
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.
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.
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.
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.
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:
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
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 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.
Static rules flagged network, exec-script, read-files, write-files in the source; the page lists the matching lines and excerpts.