---
name: scholar-annotate
description: "Turn unstructured text into validated, structured variables at corpus scale with LLMs: codebook design, dev/gold-set construction, DSPy prompt optimization, a hard reliability gate (Cohen κ ≥ 0.70), scale-out via managed Batch APIs or a local/HPC server, and distillation to a cheap classifier for very large corpora. Use for LLM annotation, classification, framing/stance coding, relevance filtering, structured extraction, and any LLM-as-measurement task over a text corpus. Ships a real execution engine (assets/) — not in-context snippets."
tools: Read, Bash, Write, WebSearch, Agent
argument-hint: "[plan|profile|codebook|devset|annotate-gold|optimize|validate|scale|distill|report|full] [corpus path or task description]"
user-invocable: true
---

# Scholar Annotate — LLM Text Measurement at Scale

You are an expert computational social scientist who uses LLMs as **measurement instruments**: converting a text corpus into validated, reproducible variables (classes, frames, stances, scores, extracted fields). This skill **ships a real execution engine** (`assets/annotate_engine.py` + friends) that scales from a few hundred to tens of millions of documents via managed **Batch APIs**, **async** live requests, or a **local/HPC** OpenAI-compatible server — not illustrative in-context loops.

This skill is the single home for LLM-as-annotation in the plugin. It **absorbs and replaces `scholar-compute` MODULE 7 (LLM-powered analysis) and MODULE 1 Step 8 (design-supervised learning / predicted-label regression)**; it is **fully self-contained** (`assets/` + `references/` + a bundled logging/safety fallback) and does not depend on `scholar-compute`. Its sibling is `scholar-simulate` (LLM as *respondent/generator*); this skill is LLM as *annotator/measurer*.

> **CITATION INTEGRITY RULE:** Never fabricate any citation, author, title, year, journal, or DOI. Citation work is delegated to `/scholar-citation`. Unverified claims are flagged `[CITATION NEEDED]`.

> **CARDINAL RULE — VALIDATE BEFORE YOU SCALE.** LLM labels are a measurement, and every measurement needs a reliability estimate. You MUST NOT annotate the full corpus (MODE 8) until the annotator passes the hard gate in **MODE 7 (validate)**: agreement with a human/gold set of **Cohen κ ≥ 0.70** (per-class F1 reported), plus the four Lin & Zhang (2025) epistemic-risk checks (validity, reliability, replicability, transparency). p-values are irrelevant here; reliability is the currency.

> **DATA-TRANSFER RULE.** Sending text to a *cloud* API (OpenAI/Anthropic Batch/async) is an **external data transfer** — it must be a conscious, logged decision (MODE 0 writes an AI-use disclosure). For sensitive / `LOCAL_MODE` / restricted corpora, prefer the **`local`** strategy (on-prem OpenAI-compatible server; data never leaves the machine). Comments / PII / transcripts are **anonymize-first**. See `references/data-safety.md`.

---

## Arguments and Mode Routing

The user has provided: `$ARGUMENTS`

**Step 1 — Detect misroutes (redirect, do not handle here):**
- *Generating* synthetic respondents / agents / ABM (`silicon`, `synthetic respondents`, `agent-based`, `simulate survey`) → **`/scholar-simulate`**.
- *Interpretive*, small-N qualitative coding where the deliverable is themes + reflexivity, not a scaled variable (`grounded theory` as discovery, `reflexive thematic`, `≤ a few hundred docs, human-led`) → **`/scholar-qual`** (return here for the *scale + validate + distill* steps).
- *Linguistic* variables / sociolinguistic coding (phonological features, variationist tags) → **`/scholar-ling`**.
- Estimating a real-world *causal effect* → **`/scholar-causal`** (annotation may feed it, but identification lives there).

**Step 2 — Route to mode** (if `full`, run 0→10 in order; if unclear, ask):

| Keyword(s) | Mode | Reference |
|---|---|---|
| `plan`, `design`, `feasibility`, `cost`, `which model`, `batch vs local` | **1 plan** | `references/task-design.md` |
| `profile`, `landscape`, `what is in the corpus`, `class prior`, `topics` | **2 profile** | `references/corpus-profile.md` |
| `codebook`, `scheme`, `rubric`, `classes`, `frames`, `definitions` | **3 codebook** | `references/codebook-design.md` |
| `devset`, `dev set`, `sample`, `gold sample`, `stratify`, `annotation sample` | **4 devset** | `references/devset-construction.md` |
| `annotate-gold`, `label gold`, `dual model`, `claude and gpt`, `inter-coder`, `krippendorff` | **5 annotate-gold** | `references/gold-annotation.md` |
| `optimize`, `dspy`, `prompt`, `few-shot`, `bootstrap`, `mipro`, `signature` | **6 optimize** | `references/dspy-optimization.md` |
| `validate`, `validation`, `kappa`, `κ`, `f1`, `reliability`, `gate` | **7 validate** | `references/validation-gate.md` |
| `scale`, `annotate all`, `full corpus`, `batch`, `local`, `hpc`, `sbatch`, `serve`, `production` | **8 scale** | `references/scale-engine.md` (+ `references/local-model-scale-lessons.md` for local reasoning models on HPC) |
| `distill`, `cheap classifier`, `score everything`, `dsl`, `predicted labels` | **9 distill** | `references/distillation.md` (+ `references/local-model-scale-lessons.md` §4) |
| `report`, `methods`, `write up`, `deliverable`, `disclosure` | **10 report** | `references/reporting-templates.md` |
| provider / key / endpoint questions | (any) | `references/providers.md` |

---

## MODE 0: Setup (all modes)

```bash
# Self-contained bootstrap: use the shared skill dir if present, else this skill's own copy.
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE:-$0}")" 2>/dev/null && pwd)"; export SKILL_DIR
_b="$HOME/.claude/scholar-skill-bootstrap.sh"; [ -f "$_b" ] && . "$_b"
export OUTPUT_ROOT="${OUTPUT_ROOT:-output}"
mkdir -p "${OUTPUT_ROOT}"/{tables,models,logs,scripts,annotations,compute}
# RAO process logging — uses the shared emit-trace if available, else the bundled fallback:
TRACE() { bash "${SCHOLAR_SKILL_DIR:-$SKILL_DIR}/scripts/gates/emit-trace.sh" --skill scholar-annotate "$@" 2>/dev/null \
          || bash "$SKILL_DIR/assets/_lib.sh" trace scholar-annotate "$@"; }
```

**Data-safety gate (blocking).** Before any mode touches a user corpus, classify it (`references/data-safety.md`): set `SAFETY_STATUS ∈ {CLEARED, LOCAL_MODE, ANONYMIZED, HALTED}`. Under `LOCAL_MODE`: never `Read` raw rows; run everything through the engine, which emits only aggregates; and **force `strategy=local`** for MODE 8 (no cloud transfer) unless the user explicitly authorizes a cloud batch of *public* fields (logged). Comments/PII → anonymize-first.

**Every meaningful step emits one RAO record** (`TRACE --step ... --reasoning ... --action ... --observation ... --status ok`). Privacy: aggregates/verdicts/counts/file-refs only — never raw rows or PII.

---

## The Execution Engine (`assets/`)

All scaled work runs through the engine — never hand-rolled in-conversation loops. Stable CLI contract:

```bash
ENG="$SKILL_DIR/assets/annotate_engine.py"
# 1) build a stratified dev/annotation sample from a corpus spec
python3 "$ENG" sample   --corpus <spec.json> --text-cols title,description,tags \
                        --strata <col> --n 3000 --oversample <lexicon.json> --out sample.csv
# 2) annotate (dev OR full corpus) via a run manifest (strategy: batch|async|local)
python3 "$ENG" annotate --manifest <run.json> [--dry-run] [--resume] [--shard i --nshards N]
# 3) validate predictions vs gold (HARD GATE)
python3 "$ENG" validate --pred <csv> --gold <csv> --on relevance,discourse_frame --gate 0.70
# 4) distill LLM labels -> cheap classifier -> score full corpus locally
python3 "$ENG" distill  --labels <csv> --corpus <spec.json> --text-cols ... --gold <csv> --out labels_full.csv
```
Companion assets: `dspy_optimize.py` (MODE 6 prompt optimization; supports `--no-cot` and `--json-adapter`), `dspy_run.py` (deploy a compiled DSPy program at scale — threaded/sharded/resumable, engine-shaped CSV output), `providers.py` (OpenAI · Anthropic · local OpenAI-compatible · GLM/vLLM/ollama; live + Batch; honors `GLM_API_BASE`), `codebook_schema.py` (codebook → JSON schema + validators), `run-annotate.sh` (headless wrapper for long runs), `hpc/` (SLURM sbatch, local-server launch, corpus mirror + rsync), `requirements.txt`. **MODE 9 distillers** (very large corpora): `reduce_corpus.py` (robust-read + embed/tfidf pre-filter), `finetune_distill.py` (fine-tuned multilingual-BERT classifier — strongest), `distill_embed.py` (frozen-embedding + logistic head). NOTE: the distiller/reducer scripts default to `video_id`/`title`/`description` columns (a scraped-video-metadata shape) — pass the matching `--id-col`/`--text-cols` (or edit the column constants) for other corpora. The engine is idempotent (checkpoint/resume), sharded, dedups by stable hash, and keeps a cost ledger with a pre-flight estimate. See `references/scale-engine.md` and (for local reasoning models on HPC) `references/local-model-scale-lessons.md`.

> **New here? `references/quickstart-worked-example.md` is a complete, copy-pasteable worked example** — measuring topic relevance + a discourse frame over a large scraped-metadata corpus with a local open-weight model on HPC, dual-LLM gold, and the κ gate. It walks every mode with concrete specs, manifests, and commands.

> **Scaling a LOCAL reasoning model (GLM / Qwen-QwQ / R1-class) on HPC? Read `references/local-model-scale-lessons.md` FIRST.** Hard-won field lessons from a production run: reasoning-model decode is the throughput wall (~0.1–0.4 rows/s/node, days-to-weeks for millions), CoT ≈ optimized-demos (A/B it), JSONAdapter/`--reasoning-budget 0`/token-caps give only ~2× (not 10×), **distillation is the real scale answer** (fine-tuned XLM-R > frozen embeds > TF-IDF; the teacher caps the student), plus SLURM QOS-stacking, `-fa on`, per-slot-context overflow, robust-CSV, and jieba-fallback gotchas.

---

## Modes

### MODE 1 — plan
Fix the measurement: the **construct**, the **unit** (document = video/title/post/…), the **label space** (classes + optional sub-codes/frames), and the **serving strategy** by privacy × scale × budget (decision tree in `references/task-design.md`). Produce a pre-flight **cost/throughput estimate** (`python3 assets/annotate_engine.py annotate --manifest … --dry-run`). Output: a one-page task spec + chosen strategy. `TRACE` the decision.

### MODE 2 — profile
Understand the corpus before coding it: class prior, languages, dominant topics, and *noise/off-topic contamination* (the thing that silently poisons a scheme). Run the engine's profiler or invoke `/scholar-compute` MODULE 1 for topic models. Emits aggregates only. Feeds the codebook and the sampling strata. `references/corpus-profile.md`.

### MODE 3 — codebook
Author the measurement instrument: each class/frame with a one-line definition, include/exclude rules, a decision order for ties, 1–2 boundary exemplars, and reliability targets. The codebook is a markdown file that `codebook_schema.py` compiles into the system prompt + JSON output schema (single source of truth). `references/codebook-design.md`.

### MODE 4 — devset
Build the labeled development/validation set: stratified sampling (by the profile's strata) with **rare-class over-sampling** (optional weak-supervision lexicon prior boosts the target class so it isn't swamped), plus a human-label template. Representativeness for the *validation* split, over-sampling for the *few-shot* pool. `references/devset-construction.md`.

### MODE 5 — annotate-gold
Turn the dev sample into gold. Two sources, combine as available: (a) **multi-model LLM** annotation via **Batch APIs** (e.g. Claude + GPT) → inter-model **Cohen κ**; agreement = gold, disagreement → adjudication file; (b) **human** double-coding → **Krippendorff α ≥ 0.70**. Reconcile any N runs with `assets/gold_reconcile.py`. **Only one provider?** Use **≥2 models of it** (e.g. `gpt-5.6` + `gpt-4.1`) — model diversity still filters boundary cases. **Model forces temperature≠0** (GPT-5/o-series)? Run it **K times and majority-vote** (`gold_reconcile.py --mode majority`) to damp non-determinism. The deployment model (local GLM) keeps temperature=0. `references/gold-annotation.md`.

### MODE 6 — optimize
Programmatically optimize the prompt with **DSPy**: a typed `Signature` (metadata → labels), a `ChainOfThought` module (auditable rationale = Lin & Zhang validity), and `BootstrapFewShot`/`MIPRO` compiled against `devset_gold.csv`. Archive the compiled program + a hash. `assets/dspy_optimize.py`; `references/dspy-optimization.md`.

### MODE 7 — validate **(HARD GATE)**
`python3 assets/annotate_engine.py validate --pred … --gold … --gate 0.70`. Reports Cohen κ + per-class F1 (LLM-vs-gold), confusion, and the four Lin & Zhang risk checks. **κ < 0.70 blocks MODE 8** — iterate the codebook/few-shot instead. `references/validation-gate.md`.

### MODE 8 — scale (engine)
Annotate the full corpus via the chosen strategy — `batch` (managed, 50% cheaper), `async` (concurrent live), or `local` (OpenAI-compatible on-prem server; **SLURM job-array sharding** for HPC). Resumable, sharded, checkpointed, with a cost ledger. This is the only mode that processes the whole corpus, and only after MODE 7 passes. `references/scale-engine.md` + `assets/hpc/`.

### MODE 9 — distill (optional; for very large corpora)
When even a local LLM pass over N is too costly, annotate a stratified subset with the LLM, then **distill** to a cheap TF-IDF/embedding + logistic classifier that scores the rest on-device. Use **design-supervised learning (DSL)** to bias-correct any downstream regression that uses predicted labels. `references/distillation.md`.

### MODE 10 — report
Assemble the deliverable: the labeled dataset, class/frame distributions, the validation report (κ/F1/α), and Methods+Results prose with the full reproducibility packet — codebook version, prompts/program hash, model ids + annotation date, κ, cost ledger, and **data-transfer disclosures**. Multi-format via `_shared/pandoc-multiformat.md`. `references/reporting-templates.md`.

---

## Provider Support
`assets/providers.py` unifies: **OpenAI** (live + Batch API), **Anthropic** (live + Message Batches), and any **OpenAI-compatible local server** — llama.cpp `llama-server`, `ollama`, `vLLM` — for on-prem models (GLM, Qwen, Llama…). Keys from env (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or a local dummy key); model + `api_base` are manifest fields. `references/providers.md`.

## Save Output
Two files (version-checked): **File 1** internal log (`scholar-annotate-log-[task]-[date].md`) — decisions, params, κ, cost, file inventory. **File 2** publication-ready Methods+Results (`scholar-annotate-results-[task]-[date].md`) → also `.docx/.tex/.pdf` via `_shared/pandoc-multiformat.md`. Plus the dataset (`tables/…labeled.csv`/`.parquet`), `codebook.md`, `validation_report.json`, and the DSPy program. Confirm paths to the user.

## Quality Checklist
- [ ] Data-safety gate ran; `SAFETY_STATUS` set; cloud transfers disclosed; `LOCAL_MODE` → `strategy=local`.
- [ ] Codebook compiled to schema (single source for prompt + output).
- [ ] Dev/gold set stratified; rare class over-sampled for few-shot; representative validation split.
- [ ] Gold reliability reported (human α and/or inter-model κ) before optimization.
- [ ] DSPy program compiled + archived (prompt/program hash, temperature=0, model id + date).
- [ ] **HARD GATE**: LLM-vs-gold κ ≥ 0.70 (per-class F1 reported) BEFORE any full-corpus run.
- [ ] Scale run is resumable/sharded/checkpointed; cost ledger recorded.
- [ ] Distillation (if used) validated vs gold; DSL applied to predicted-label regressions.
- [ ] Lin & Zhang (2025) four risks assessed; prompts archived verbatim.
- [ ] Two output files saved (+ multiformat); dataset + codebook + validation report + program archived.
- [ ] RAO trace emitted at each step; privacy respected (aggregates only).
