Skip to content
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/44 lessons0/13 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 no general-purpose editor ships out of the box: 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 watches for bak-saturation — the verb 박다 landing two or more times in one paragraph — where softer choices fit the register. That's not a grammar error, and no general tool ships with it. It's a voice rule. A configurable prose linter (Vale is the well-known one) could be taught a rule like it — the engine was never the scarce part. What no tool ships is the doctrine: one writer's rules, written down from years of real corrections and kept in step with them. A voice checker is that doctrine wired into the place the writing happens. (The live wiring is opt-in: the full voice set runs under the pippa voice profile; the default prose pass keeps a narrow subset like this one.)

The Single Detection Boundary

Mechanically, it's a mark plus a hover note: underline the offending range (mark), and the explanation floats as a tooltip when you hover — not an inserted 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 feature no general editor ships (a checker for one writer's doctrine) needed no new rendering machinery of its own — the work went into the doctrine and the detection, not into drawing the underline.

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()).map((issue) =>
    // the note rides on the mark itself (a title attribute shows it on hover);
    // a richer bubble would use CM6's hoverTooltip — still not an inserted widget
    Decoration.mark({
      class: "cm-voice-issue",
      attributes: { title: issue.note },
    }).range(issue.from, 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 in — Please sign in to comment.

No comments yet — be the first.