Files
harness-tools/skills/document/unslop/references/mimic.md
T
2026-08-24 16:33:11 -04:00

195 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Teach & Mimic — internals
The routed flows live in `references/commands/teach.md`,
`references/commands/mimic.md`, and the refine section of that mimic command
file. This file holds the deep material those flows link to: card anatomy,
sample requirements, scoring, baselines, statistics, and failure modes.
Reconstruction runs under detection's constitution: a mimic or refined output
that reintroduces slop is a failure. Every artifact here is deterministic and
testable; the model is used only to generate prose, never to score itself.
## Teach — the artifacts
`teach` distills a writer's samples into a reusable voice, stored under
`.unslop/voice/<name>/` (gitignored by default):
1. `scripts/voice_profile.py samples/ -o profile.json` — the machine
fingerprint (char 3-grams, function-word delta, sentence-length EMD,
punctuation, contractions, MTLD, word-length, impostor z-scores, GI rank).
This is the referee the scorer uses.
2. `scripts/voice_card.py --profile profile.json --samples samples/ --out .
--name <name>` — the **layered voice card** the generating model actually
follows. `--name` labels the card (`# Voice card: <name>`); default `voice`.
voice_card recomputes the profile from `--samples` and refuses (exit 2, named
field) if the supplied `--profile` does not describe those samples, so a stale
profile can never silently drive a card.
3. Add `--provenance` to write `provenance.json` (per-sample sha256, word
counts, doc count, genre note, low-confidence flag) so a teach run is
auditable.
**Sample files.** Both scripts read only `.txt` and `.md` files, recursively,
under the samples directory (nothing else — a directory of `.docx` or extensionless
files reads as empty and both scripts exit 2 with a diagnostic naming the
requirement). Convert samples to `.txt`/`.md` first.
**Sample requirements.** Use at least 5 documents, 2-3k words total, in the
**same genre** as the target. Fewer than ~2000 words or cross-genre samples set
`low_confidence` in the profile and provenance; the card still builds but its
claims are noisier. **Surface `low_confidence` to the user** — tell them the voice
is provisional and ask for more same-genre samples before trusting a mimic.
Cross-domain style attribution degrades sharply, so do not teach on blog posts
and score legal memos.
## The voice card (layered)
The card is a directory, not one file, so the generator loads only what the
current writing task needs:
- `card.md` — the always-loaded core, kept under 300 words: rhythm (median
sentence length, IQR, burstiness), contraction habits with real examples,
punctuation the writer uses and avoids, top sentence openers, a **Never**
list of at/near-zero features, and an **index table** — "when writing X, read
`card/X.md`".
- `card/<situation>.md` — one sheet per **covered** situation from the taxonomy
(explaining-technical, anecdote, argument, disagreement, praise,
hedging-uncertainty, numbers-data, addressing-reader, openings, closings).
Each sheet gives how this author handles that situation, 1-3 verbatim sample
snippets, and measured markers.
**No fabrication.** A dimension with no sample evidence gets **no sheet**; it is
named under "Uncovered" in `card.md`. The card never invents a voice the
samples cannot support.
**Coverage classification.** `voice_card.py --coverage` emits a deterministic
lexical coverage matrix over the taxonomy. It is intentionally coarse: it
DRIVES which sheets get written and which teach prompts to ask the user, and a
misclassified sentence can only add or drop a sheet — it can never change a card
claim, because claims come from measured facts and verbatim snippets. So
misclassification is low-stakes by construction. A sample set missing numeric
writing, for instance, leaves `numbers-data` uncovered and named as a gap.
**Coverage → prompt procedure.** When a dimension the target writing will need is
uncovered, ask the user for a short sample that exercises it. **Only ask for
dimensions the task requires** — do not fish for all ten. Templates:
| Dimension | Prompt to the user |
|-----------|--------------------|
| explaining-technical | "Share something you wrote that explains how a thing works or why it behaves the way it does." |
| anecdote | "Share a few sentences where you tell a small story about something that happened to you." |
| argument | "Share something you wrote that argues a position — a claim you defended." |
| disagreement | "Share something where you pushed back on or disagreed with an idea." |
| praise | "Share something where you praised or recommended something you liked." |
| hedging-uncertainty | "Share something where you were unsure and said so — thinking out loud." |
| numbers-data | "Share something you wrote that works with numbers, quantities, or measurements." |
| addressing-reader | "Share something written directly to a reader — instructions or a note to someone." |
`openings` and `closings` are structural (first/last sentence of every document)
and are covered as soon as there is one document, so they need no prompt.
The card is pack-sized (~1-2k tokens loaded) so it rides in the generation
context cheaply. Same inputs give byte-identical files.
## `--refine` — the internal hill-climb
`evals/run_mimic_refine.py` iterates when one pass is not enough. It is worth
the cost only when a single mimic keeps landing short of the target voice; most
users teach once and mimic cheaply thereafter.
Protocol (climb on A, accept on DEV, report on a sealed split):
- Split samples by document (seeded): **A** (~60%, retrieval/context pool) and
**DEV** (~40%, acceptance). Fewer than 5 docs → refuse (exit 2).
- **LIVE (default):** each iteration assembles B prompts — the draft, the
A-split voice card, the k=2 nearest-A samples by char-3gram cosine, and the
current directives — and invokes `--generate-cmd` (default `claude -p`) once
per beam, piping the prompt on stdin and reading the candidate on stdout.
`--baseline zero|few|retrieval` selects which samples ride in the prompt (none
/ first-k / k nearest-A) and generates its candidate set through this same
path, so the honest retrieval-few-shot competitor and the refine loop share
one pipeline.
- **DRY-RUN:** pass `--candidates-dir DIR` and the loop reads
`DIR/iter<i>/*.md` as that iteration's batch instead of generating them, so
the loop logic runs with no model calls (this is how the MIMIC-* rows exercise
it; the LIVE path is covered by a mock generator).
- **Hard gates** discard a candidate regardless of score: banned-phrase clean,
structure clean, draft→candidate preservation, no copy-gate violation against
A, and a 150-word floor.
- Survivors are scored against DEV (and recorded against A for the divergence
guard) with the **full `voice_score` composite** — `0.5·(1−GI) + 0.5·` clipped
weighted impostor-z distance against a same-genre impostor pool (`--impostors`,
default the committed pool), seeded. This is deliberately NOT a raw weighted
distance: a marker-stuffed candidate can minimize a raw distance and clear
every hard gate yet be exposed by GI, so raw-distance acceptance would accept
it over honest prose. The generator never scores itself.
- Accept an iteration only if the DEV composite improves by ≥ `min-delta`; the
best-so-far never regresses.
### Stopping and the divergence guard
Earliest of: iteration cap, patience (N iterations without a DEV gain), or
**divergence** — the A composite improving while DEV worsens for two
consecutive iterations. Divergence is the reward-hacking signature (the model
is fitting the retrieval pool's topics, not the author's style); the loop halts
immediately and sets `reward_hacking_warning`.
### Directives and card refinement
Each iteration derives up to four ranked directives from the DEV metric deltas,
and each directive carries a proposed **card amendment** line. The best
candidate is copied to `OUT/final.md` and the amended card to
`OUT/voice-card.refined.md`.
## Scoring
The `voice_score` **composite** the refine loop accepts on is
`0.5·(1−GI) + 0.5·zsum` (lower = more author-like), where `GI` is the General
Impostors rank — the fraction of random feature-subset trials in which the
candidate beats every sampled impostor at looking like the profile — and `zsum`
is the clipped, weighted **impostor-z** distance (each per-feature distance
normalized by the impostor pool's mean/std, clipped to ±3, weighted by the WP10a
research weights). Both halves are calibrated against a same-genre **impostor
pool**, so the number rewards *beating plausible other authors*, not merely
matching aggregate surface stats. That is what defeats a marker-stuffed
candidate: it can drive a raw weighted distance down and still be far worse under
GI. Meaning preservation vs the original draft is a **separate** hard gate, never
blended into the composite. The composite is a **guide, not an oracle** — even
strong authorship verification tops out near a 93-94% ceiling, so treat
per-candidate deltas as the signal and under-claim.
## Baselines
Score refine output against paired baselines on the same drafts: zero-shot
("write like this person"), static few-shot, and **retrieval few-shot** (k
nearest-A samples — the honest competitor). Refine has to beat retrieval
few-shot to earn its cost.
## Statistics
`evals/mimic_stats.py` runs the small-n discipline: per-item paired deltas, a
BCa bootstrap CI (n=2000, seeded), and a sign-flip permutation test (exact for
≤12 items, else 20k seeded). Claim a win only when the CI lower bound > 0 **and**
p < 0.05. Anything degenerate (too few items, zero variance) reports
not-improved.
## Cost
| Step | Calls |
|------|-------|
| teach (profile + card) | free / deterministic |
| mimic | one strong generation call |
| refine | iterations × beam strong calls, plus free gate/score passes |
## Failure modes and the gate that catches each
| Failure | Symptom | Caught by |
|---------|---------|-----------|
| Topic bleed | importing what samples are *about*, not *how* they read | divergence guard (A improves, DEV worsens) |
| Verbatim copying | lifting sample phrasing | copy-gate (4-gram / LCS vs A) |
| Register lock | latching one sample's register | DEV composite vs held-out split |
| Reward hacking | self-assessed quality rising while DEV falls | divergence guard + `reward_hacking_warning` |
| Slop reintroduced | AI tells creep back into voiced prose | banned-phrase / structure gates |
For chunking long documents and the tiered execution architecture around these
steps, see `pipeline.md`.