C.W.K.
Stream
Lesson 03 of 05 · published

Job Two — The Voice Underline

~12 min · voice-underline, voice-doctrine, mark-decoration, job-two

Level 0Cold Draft
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A spell checker knows the dictionary. A voice checker knows you. Rekindle's second decoration job is the second kind."

Job Two — a Voice Checker, Not a Spell Checker

The second job of the decoration engine is one nothing else in the world ships: a live voice checker. As you write, it underlines phrases that trip a voice rule — a register slip, a token-mirror trap, an over-forceful word choice — and floats a small margin note explaining why. A spell checker measures your text against a dictionary everyone shares. A voice checker measures it against your doctrine: the specific traps and preferences one writer has decided matter. It's the same underline-in-place UX as spell check, pointed at something personal.

A concrete example from the doctrine: a documented Korean rule flags 박다 — an over-forceful verb — where a softer choice fits the register. That's not a grammar error; no general tool would ever catch it. It's a voice rule, and only a voice checker built around one writer's doctrine can enforce it.

The Single Detection Boundary

Mechanically, it's marks plus a widget: underline the offending range (mark) and attach a note beside it (widget). All the intelligence hides behind one function, detectVoiceIssues(doc), which returns character ranges and notes. Today the detection is deterministic rules in TypeScript; later it can move to a Rust voice-engine substrate, or grow an AI-judgment layer — and the decoration code won't change, because it only ever consumes ranges. The boundary is the whole trick: keep 'what counts as an issue' behind one function, and 'how we draw it' stays identical no matter how detection evolves.

type VoiceIssue = { from: number; to: number; note: string };

// Detection is one boundary. Rules today; Rust or AI-judgment later —
// the decoration layer never changes, because it only consumes ranges.
function detectVoiceIssues(doc: string): VoiceIssue[] { /* ... */ }

Same Engine, New Purpose

Step back and notice: this is the exact machinery of Live Preview, aimed at a different question. Live Preview asked "where is markup?" and drew marks. Voice underline asks "where is a voice issue?" and draws marks plus a note. Same primitive, new input. That's the track's thesis arriving for the second time — and the reason a genuinely novel feature (a voice checker that exists nowhere else) cost almost no new machinery to build.

Code

Detection behind one boundary; decoration consumes ranges·typescript
type VoiceIssue = { from: number; to: number; note: string };

// The ONLY thing that knows what counts as a voice issue.
// Deterministic rules today; a Rust substrate or AI-judgment layer later.
function detectVoiceIssues(doc: string): VoiceIssue[] {
  // e.g. flag an over-forceful verb where the register wants a softer one
  return scanVoiceRules(doc);
}

// The decoration builder never changes when detection evolves:
function buildVoiceDecorations(state) {
  return detectVoiceIssues(state.doc.toString()).flatMap((issue) => [
    Decoration.mark({ class: "cm-voice-issue" }).range(issue.from, issue.to),
    Decoration.widget({ widget: new MarginNote(issue.note), side: 1 }).range(issue.to),
  ]);
}

External links

Exercise

Invent one voice rule for your own writing — a word you overuse, a register you slip into, a phrase you want to catch. Now describe how it'd surface as a decoration: what range gets underlined, and what does the margin note say? Notice you've just specified a feature no general tool ships, using the same mechanism as Live Preview.
Hint
Frame the rule as a function returning ranges + notes: given the document text, where's the issue (from/to) and what's the one-line reason? That's detectVoiceIssues for your voice — the decoration code is already written.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.