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

The Cleanup Contract

~12 min · cleanup-contract, insertion, safety, provider

Level 0Unlit
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The insertion layer types whatever it's handed. So never hand it a sentence a chatbot wrote."

A Transform, Not a Conversation

Cleanup is a strict transform: raw transcript in, polished insertable text out. It must never answer the transcript, add content, or editorialize. If you dictate "remind me to call the bank," cleanup returns "Remind me to call the bank." — punctuated and capitalized — not "Sure, I'll remind you!" The provider receives a structured request (raw transcript, mode, any selected text, allowed context, dictionary, snippets, style profile, language hints, output constraints) and returns a structured result. Both ends are typed on purpose.

Why the Result Must Be Structured

The insertion layer is dumb by design: it types whatever text it's given into the live app. That's exactly why cleanup can't hand it free prose. If a cleanup brain returned "Here's your cleaned text: …" or "I'm not sure what you meant," that would be typed into the user's document verbatim. The structured result — text, action, press_enter, warnings — guarantees the insertion layer only ever receives the fields it knows how to act on. Prose is not a valid return value; a result object is.

CleanupRequest  { transcript, mode, selection?, context?, dictionary,
                  snippets, style, language, constraints }
        |
        v   (provider: cwkPippa full / local Ollama / deterministic)
        v
CleanupResult   { text, action: "insert", press_enter, warnings[] }
        |
        v
Insertion       types ONLY result.text -- never raw model prose

Taming Weak Models

Local models are the hard case: a small Ollama model may ignore your instruction to return JSON and reply with a markdown-fenced blob, a preamble, or an emotion tag. The provider's job is to tame that — strip fences and preambles, and if all else fails, wrap the raw text in a valid result object so the contract still holds. The rule is: the insertion layer never sees the mess. Whatever a weak model does, the adapter normalizes it into the same clean result shape the strong models produce.

Type both ends of anything that feeds a live effect. When a component's output goes somewhere irreversible — typed into an app, sent to a device, written to disk — its input and output must be structured contracts, not free text. The structure is what stops a chatty or confused model from doing damage through a downstream layer that trusts whatever it's handed.
Never let the cleanup brain answer the user. The single most important cleanup rule is that it transforms, never responds. A model that "helpfully" answers your dictated question instead of cleaning it will type its answer into your email. Preserve meaning, fix mechanics, and return — no content, no replies, no follow-up questions.

Code

Structured in, structured out — with a wrap-fallback for weak models·typescript
interface CleanupRequest {
  transcript: string;
  mode: "dictation" | "command" | "transform";
  selection?: string;
  context?: string;         // opt-in nearby text, never secure fields
  dictionary: DictEntry[];
  language: "ko" | "en" | "auto";
}

interface CleanupResult {
  text: string;             // the ONLY thing insertion will type
  action: "insert";
  press_enter: boolean;
  warnings: string[];
}

// A weak local model may return fenced prose. Normalize it:
function toResult(raw: string): CleanupResult {
  const text = stripFencesAndPreamble(raw);
  return { text, action: "insert", press_enter: false, warnings: [] };
}
// Insertion always receives a CleanupResult, never raw model output.

External links

Exercise

Design the result type for a feature where a model's output feeds an irreversible action in your app. List the fields the downstream layer needs, then write the one sentence describing what your adapter does when the model ignores the format and returns chatty prose instead.
Hint
The downstream layer needs exactly the fields it acts on and nothing else. When the model misbehaves, the adapter's job is to normalize — strip the chatter, extract the usable part, and wrap it in the valid result shape — so the irreversible action never receives raw, unvalidated model text.

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.