<!-- Given away by The Rabble Group (rabblegroup.com) as part of "How to build a content engine." This is the engagement-agnostic voice-learner skill: it names no client and loads the engagement's own context files. Adapt to your business. No license, take it. -->

---
name: voice-learner
version: 1.0.0
description: >
  Detects patterns in human editing behavior between AI-produced drafts and
  human-published versions. Diffs content/drafts/draft-X.md against
  content/published/<slug>.md for newly-published posts. Categorizes edits,
  aggregates patterns across recent diffs, and proposes updates to voice
  profile files. Always human-gated — never auto-mutates voice files.

  The learning loop: writer produces v1 → human edits in WP → site-sync
  captures the published version → voice-learner diffs and proposes updates
  → human reviews and applies. Over time, the voice profile becomes a
  living document trained on actual ship-quality writing.

inputs:
  required:
    - content/drafts/draft-*.md       # AI-produced source
    - content/published/<slug>.md     # human-edited published version
    - context/voice/core.md           # current voice — for comparison and proposal targets
    - context/voice/blog.md           # blog overlay
  optional:
    - context/voice/linkedin.md       # channel overlay (when LinkedIn writer ships)
    - context/content-anti-patterns.md  # current anti-patterns — for proposal targets
    - _state/voice-learner-patterns.json  # accumulated patterns across runs
    - outputs/voice-update-proposals.md   # proposals not yet applied

outputs:
  - outputs/voice-learning-report-YYYY-MM-DD.md   # weekly report
  - outputs/voice-update-proposals.md              # accumulated proposals (overwrite on each run)
  - _state/voice-learner-patterns.json             # updated pattern registry

upstream: [site-sync (provides content/published/ snapshots)]
downstream: [human (reviews and applies proposals), analytics-tracker (integrates editorial signal with performance data)]

cadence: weekly cron (Saturday morning, before Sunday's radar)

status: active
pipeline: content-engine
---

# Voice Learner

You detect patterns in human editing of AI-produced drafts and propose updates to the voice profile. You never auto-mutate voice files — you propose, humans apply.

The premise: when a human consistently edits "comprehensive evaluation" → "full evaluation" across multiple posts, the voice profile is wrong (or stale). Better to add "comprehensive" to banned words once than for the writer to keep producing it and the human to keep editing it out.

This skill makes that pattern detection observable.

---

## Before You Run

### Required reads

1. **Current voice files** — `context/voice/core.md` and `context/voice/blog.md`. You'll cross-reference proposed updates against these.
2. **Current anti-patterns** — `context/content-anti-patterns.md`. Some proposals are additions to this file rather than to voice files.
3. **All draft + published pairs** — find files in `content/drafts/draft-*.md` where the corresponding `content/published/<slug>.md` exists. The pair is the diff input.
4. **Existing pattern registry** — `_state/voice-learner-patterns.json`. Tracks patterns observed across runs. New observations accumulate; thresholds determine when a pattern becomes a proposal.
5. **Outstanding proposals** — `outputs/voice-update-proposals.md`. Patterns already proposed but not yet applied or rejected.

### Pair drafts with published versions

For each `content/published/<slug>.md`:
1. Read its frontmatter — `source_draft` field points to the draft file
2. Confirm the draft exists at `content/drafts/<source_draft>`
3. If yes: this is a diff pair
4. If no: log anomaly ("published snapshot has no matching draft") and skip

Process only pairs where the draft's frontmatter `status` is `published` (set by site-sync) and the published snapshot's `captured_at` is newer than the previous voice-learner run.

---

## Step 1: Diff Each Pair

For each pair, compute a structured diff at multiple granularities.

### 1A: Strip both files to body text

Voice-learner cares about body content, not frontmatter or delivery notes.

For the draft:
- Strip the YAML frontmatter (everything between the first two `---` markers)
- Strip the `## Delivery notes` section (everything from that heading to end of file)
- Strip the H1 title line (titles get edited but rarely indicate voice patterns — handle separately)

For the published snapshot:
- Strip the YAML frontmatter
- Keep the H1 (will compare against draft H1 separately)

### 1B: Word-level diff

Use `git diff --word-diff` or equivalent to identify word-level changes. Save to `/tmp/word-diff.txt`.

Categorize changes:
- **Pure deletions:** word(s) removed without replacement
- **Pure additions:** word(s) added without removal
- **Substitutions:** word(s) replaced with other word(s) in the same position
- **Reordering:** same words, different order

### 1C: Paragraph-level diff

For paragraphs that are substantially rewritten (>40% of words changed) or completely added/removed, flag at the paragraph level rather than word level. Section additions or section cuts also fall here.

### 1D: Title diff

Compare the draft's H1 against the published H1. If different, log the change separately — title edits are high-signal for SEO/positioning and worth their own pattern category.

### 1E: LLM-based categorization

The word and paragraph diffs are mechanical. Categorization is semantic — what KIND of edit is each one? Run the diff through an LLM with this categorization prompt:

> For each edit in the following diff, categorize it as one of:
> - **banned-phrase-swap** — a phrase the voice profile bans (or should ban) was edited to its allowed equivalent
> - **tightening** — wordy phrase replaced with concise equivalent, no semantic change
> - **factual-fix** — a claim, statistic, or specific reference was corrected
> - **tone-shift** — the tone changed (more formal, less formal, more clinical, more warm)
> - **structural** — paragraph reordering, section addition/removal, heading change
> - **CTA-edit** — call-to-action wording or placement changed
> - **citation-fix** — source URL changed, attribution added, or attribution removed
> - **personalization** — generic phrasing made specific to the engagement (e.g., "the clinic" → "our team")
> - **opening-rewrite** — the validating opening paragraph was substantially rewritten (high-signal pattern)
> - **other** — none of the above; describe in a sentence
>
> Output as JSON with: edit_text_before, edit_text_after, category, confidence (0-1), one-line rationale.

Save the categorized diffs to `/tmp/categorized-diffs.json` for this post.

---

## Step 2: Update the Pattern Registry

`_state/voice-learner-patterns.json` is the accumulated history of edit patterns across runs.

**Schema:**

```json
{
  "patterns": {
    "<pattern-id-slug>": {
      "category": "banned-phrase-swap | tone-shift | etc.",
      "description": "human-readable description",
      "observed_in_posts": ["draft-2026-05-21-...", "..."],
      "first_observed": "YYYY-MM-DD",
      "last_observed": "YYYY-MM-DD",
      "occurrence_count": N,
      "examples": [
        { "before": "...", "after": "...", "post": "..." }
      ],
      "proposed": true | false,
      "applied": true | false,
      "applied_at": null | "YYYY-MM-DD"
    }
  },
  "last_run": "YYYY-MM-DD"
}
```

For each categorized edit:
1. Generate a pattern_id (e.g., `phrase-swap-comprehensive-to-full` for "comprehensive evaluation" → "full evaluation")
2. If pattern_id exists in registry: increment `occurrence_count`, append to `observed_in_posts`, update `last_observed`, append to `examples` (cap at 5 to keep file size reasonable)
3. If new: add the entry with `occurrence_count: 1`, `proposed: false`, `applied: false`

---

## Step 3: Promote Patterns to Proposals

A pattern becomes a **proposal** when it crosses a threshold. Thresholds by category (tuned conservatively for v1.0.0; revisit after first 20+ posts):

| Category | Threshold | Reasoning |
|---|---|---|
| banned-phrase-swap | 3+ occurrences in last 10 posts | Phrases consistently edited out are the strongest signal |
| tightening | 5+ occurrences with the same source phrase | Phrasings that humans always shorten deserve a voice note |
| tone-shift | 4+ occurrences in same direction | Single tone shifts can be reviewer preference; pattern across 4 is voice drift |
| opening-rewrite | 3+ occurrences out of 5 posts | Opening rewrites are expensive; if more than half need them, the validation pattern is off |
| CTA-edit | 3+ occurrences with similar source CTA | CTA patterns matter for conversion |
| structural | 4+ occurrences in same direction | Cutting/adding the same kind of section repeatedly indicates an outline-level fix |
| personalization | 2+ occurrences | This is voice drift — should be lower threshold |
| factual-fix | n/a (never propose) | Factual fixes don't change voice; they correct content |
| citation-fix | n/a (never propose) | Same; citation pattern is brief-level not voice-level |
| other | n/a (never auto-propose) | Manual review only |

When a pattern crosses threshold, set `proposed: true` in the registry and add to the proposals file.

---

## Step 4: Write the Proposals File

Output: `outputs/voice-update-proposals.md`. Overwrite on each run with the current state of all proposed-but-not-applied patterns.

Format:

```markdown
# Voice Profile Update Proposals

**Generated:** YYYY-MM-DD by voice-learner
**Total proposals awaiting review:** N

These proposals are observed patterns in human editing of published posts. Each is suggested as an update to a voice profile file or anti-patterns file. **Voice-learner does not apply these automatically. Human reviews and applies (or rejects) each one.**

To apply a proposal: edit the suggested file as described, then mark the proposal as `applied: true` in `_state/voice-learner-patterns.json` and bump the voice file's `version` frontmatter field.

To reject a proposal: delete it from this file and mark as `applied: false, rejected: true` in the registry.

---

## Proposal 1: <one-line summary>

- **Pattern ID:** <pattern-id>
- **Category:** <category>
- **Observed in:** N posts (first: YYYY-MM-DD, last: YYYY-MM-DD)
- **Suggested update:** <which file, what change>

**Examples observed:**
- In `draft-2026-05-21-...`: "<before>" → "<after>"
- In `draft-2026-05-28-...`: "<before>" → "<after>"
- (up to 5 examples)

**Why this is worth a voice profile update:** <one paragraph from the LLM categorization rationale, or the user's review notes>

**Suggested file change:**
```
File: context/voice/core.md, Banned Words & Phrases section
Add: - "<phrase>" — replaced by editors with "<replacement>" in N posts. Add to banned list.
```

---

## Proposal 2: ...
```

**Critical:** the suggested file change is specific (file path, section, exact text to add). Humans should be able to apply by copy-paste with minor review, not by reinterpreting the proposal.

---

## Step 5: Write the Weekly Report

Output: `outputs/voice-learning-report-YYYY-MM-DD.md`. New file each run; never overwrite older reports.

Format:

```markdown
# Voice Learning Report — YYYY-MM-DD

**Posts analyzed this run:** N (published since YYYY-MM-DD)
**Pattern registry size:** N total patterns tracked, N currently proposed

## Edit Volume Summary

| Post | Edit Count (word) | Categories | Notes |
|---|---|---|---|
| <slug> | N | banned-phrase-swap, tightening, opening-rewrite | <one-line> |
| ... | ... | ... | ... |

**Median edit count this week:** N words per post
**Trend vs last 4 weeks:** rising | stable | falling

## Top Edit Patterns This Week

(Patterns that increased in occurrence_count this run, even if not at threshold yet.)

- `<pattern-id>` (<category>): now N occurrences, +N this week. <one-line description>

## New Proposals (crossed threshold this run)

(Same content as outputs/voice-update-proposals.md, but only the NEW ones — patterns that just crossed threshold this run. Humans should review these first.)

- Proposal: <pattern-id> — <one-line>

## Outstanding Proposals (from prior runs)

(Patterns still awaiting human review. List with link to the proposals file.)

- N proposals awaiting review in outputs/voice-update-proposals.md

## Anomalies

- <published-snapshot-with-no-matching-draft>
- <draft-with-no-published-snapshot-but-status-published>
- (Any matching issues from Step 0)

## Performance Integration (placeholder)

Once analytics-tracker is wired up, this section will integrate edit patterns with GSC/GA4 data. Questions to answer:

- Posts with heavy edits — do they outperform light-edit posts? (Edit volume vs. organic traffic)
- Are edited phrases (e.g., "comprehensive" → "full") in higher-performing posts?
- Posts with opening-rewrites — what's their bounce rate vs. minimally-edited posts?

Currently: not enough published-and-trafficked posts to integrate. Will populate after 30+ days of published content.

## Voice Profile Drift Note

If the voice profile has been updated since the last voice-learner run (detected via frontmatter `version` field change), note here. Recent voice updates may invalidate some pending proposals — those should be reviewed first to see if they're still relevant.

**Voice files currently tracked:**
- `voice/core.md` — version: <N>, last updated: YYYY-MM-DD
- `voice/blog.md` — version: <N>, last updated: YYYY-MM-DD
- `voice/linkedin.md` — version: <N>, last updated: YYYY-MM-DD
- `content-anti-patterns.md` — version: <N>, last updated: YYYY-MM-DD
```

---

## Voice File Versioning

Voice files get a `version` frontmatter field. Bump on every applied change.

Add this frontmatter to the top of `voice/core.md`, `voice/blog.md`, `voice/linkedin.md`, and `content-anti-patterns.md` (if not already present):

```yaml
---
version: 1
last_updated: 2026-05-21
update_log:
  - date: 2026-05-21
    change: "Initial version from marketing docs"
    applied_proposals: []
---
```

When a human applies a proposal:
1. Make the suggested change to the voice file
2. Bump `version`
3. Update `last_updated`
4. Append to `update_log` with the proposal ID
5. Mark the proposal `applied: true` in `_state/voice-learner-patterns.json`
6. Remove the proposal from `outputs/voice-update-proposals.md`

Voice-learner reads the `version` field to detect updates between runs. If a voice file's version changed, it notes in the next report that recent proposals may need revalidation against the new voice baseline.

---

## What This Skill Does NOT Do

- Does NOT modify voice files. Ever. Even high-confidence proposals require human application.
- Does NOT diff title H1s into voice patterns by default (titles are SEO-driven, not voice-driven). Title edits get a separate category note in the report but don't become voice proposals.
- Does NOT diff sections that aren't part of voice (the FAQ Q&A pairs, the disclaimer text, the byline). Voice-learner is for body prose patterns.
- Does NOT respond to one-off edits. A pattern requires multiple occurrences (see thresholds in Step 3).
- Does NOT integrate performance data. That's analytics-tracker's job. Voice-learner's reports leave a placeholder section that analytics-tracker fills in later.
- Does NOT capture published content. That's site-sync's job. Voice-learner consumes what site-sync wrote.

---

## Failure Modes

| Symptom | Cause | Recovery |
|---|---|---|
| No published snapshots to diff | site-sync hasn't captured content/published/ yet, or no posts have transitioned to published in this window | Skip the run, log "no new pairs"; will run again next week |
| Anomaly: published snapshot has no matching draft | The draft was published via a different path (manual post creation in WP, not via blog-writer) | Log; voice-learner can still process the post but flags it as "unpaired" for review |
| LLM categorization disagrees with mechanical diff | Edit is ambiguous, LLM judged it differently than expected | Trust the LLM categorization but flag low-confidence (< 0.7) results in the report for human review |
| Same phrase repeatedly proposed but never applied | Human is rejecting or ignoring the proposal — pattern keeps recurring | Check `_state/voice-learner-patterns.json` for `applied: false, rejected: true`; respect rejection by NOT re-proposing within 30 days |

---

## Manual Invocation Pattern

For testing or off-cycle runs:

```
Read skills/voice-learner.md and execute it.
Use content/published/ as the input source.
Show me a summary of new proposals after completion.
```

The skill is idempotent: running twice without new published content produces no new proposals and updates the registry's `last_run` only.
