PLUGIN DETAIL FLAVOR: QUOTES MAP FLAVOR

hook-dsh-normalize-quotes

Hook @ DSH @ Normalize @ Quotes • _The DeepSeek Harness Plugin Family for PlayForm._ The quote normalizer for model output - a DeepSeek Harness plugin that hooks the llm/stream waterfall (the interceptable wrapper around EVERY streaming model call, bound to the LlmRuntime) and normalizes the curly quote family in model output, live in the transcript: the eight typographic quote code points each map to their ASCII straight counterpart - U+2018/U+2019/U+201A/U+201B to the ASCII apostrophe, and U+201C/U+201D/U+201E/U+201F to the ASCII double quote. A MAP flavor of the normalize family: the core's Quotes char-to-char table owns the substitution - no replacement config knob. The family's raw-write tool (registered by hook-dsh-normalize-dash) bypasses this flavor's transforms too - the exemption is family-wide.

CLI INSTALL COPIED
$ pnpm add @playform/hook-dsh-normalize-quotes
Namespace: @playform/hook-dsh-normalize-quotes Release: v0.0.1
Archetype: Hook Event: llm/stream Table: core Quotes MAP

The profile wiring for this plugin - the bundles list, the patch entry and the restart - is on the setup page.

Where It Fits

FAMILY POSITION: 2ND OF 6 STREAM NORMALIZERS

Family position (the @-sentence Hook @ DSH @ Normalize @ Quotes): a hook child of the plugin-dsh-factory service and the hook-dsh-core machinery; the second of the six stream normalizer siblings:

FlavorTableSubstitution
hook-dsh-normalize-dashcore Dashes class→ replacement (default -)
hook-dsh-normalize-quotes (this bundle)core Quotes MAPcurly → straight
hook-dsh-normalize-ellipsiscore Ellipsis classU+2026 → ...
hook-dsh-normalize-spacescore Spaces classunicode spaces → " "
hook-dsh-normalize-invisiblecore Invisible classremoved (default "")
hook-dsh-normalize-fullwidthcore Fullwidth MAPfull-width → half-width

A non-manifest factory consumer: it injects ["pluginFactory"] and uses only State (cell unwrap + shared Ledger/Enabled mappings + its own fields) and Append; the config is composed by the factory's standalone Schema helper with shared: false - the minimal block, no fs/observed dead fields. It touches no files, so fs/write-intent and fs/observed never see it; it wraps the downstream result and always calls next(), so it composes with other llm/stream listeners regardless of registration order.

In the DeepSeek Harness

WHERE THE FLAVOR OPERATES
SeamWhat the plugin does thereWhat you can observe
llm/stream - the model stream waterfallThe plugin's listener wraps the interceptable waterfall around EVERY streaming model call (bound to the LlmRuntime): next() is called first, options are never touched, one chunk in - one chunk out, upstream throws propagate.Straight quotes reach the live UI and the durable transcript as the stream is born.
The model stream vocabulary (dsh-llm)The chunk/block shapes it rewrites come from the harness's stream vocabulary (@deepseek-ai/dsh-llm, type-only): text deltas, reasoning deltas and assembled blocks must agree.No inconsistencies between deltas and blocks for downstream consumers.
The factory serviceA non-manifest factory consumer: State for the config and Append for every ledger line; it touches no files, so fs/write-intent and fs/observed never see it.Composes with other llm/stream listeners regardless of registration order.
The raw-write exemption (family-wide)The family's raw-write tool (registered by hook-dsh-normalize-dash) passes through this flavor's stream transforms by identity - the tool's explicit normalize parameter is the only normalization it applies.Per-call control stays with the agent, even with every stream flavor armed.
The ledger / sessionTwo lines through the factory's Append: the activation proof from apply() and the per-stream count line on a normal completion with N > 0.A thrown-away stream writes no ledger line.

The Problem

WHY THE QUOTES FLAVOR EXISTS

Smart-quote processors and code editors emit typographic quotes; every parser, shell and diff understands the straight ASCII ones. Left in model output, a curly quote is a silent correctness hazard - in code blocks, commands and file paths it is simply the wrong character. This flavor makes the straight form the one that reaches the transcript.

How It Works

THE STREAM PIPELINE
llm/stream waterfall (options, next) the interceptable wrapper around │ EVERY streaming model call ▼ next() called FIRST, always - options never touched Normalize(upstream, state) the async generator │ for await (chunk of upstream) ▼ CoreChunk(chunk, transform, reasoning, toolArgs, raw) the core's per-chunk dispatch │ ├─ text-delta ────────► ReplaceMap(text, Quotes) rewrite the text field ├─ reasoning-delta ───► same, when normalizeReasoning (default ON) ├─ block-end ─────────► the assembled block's text fields - │ TextBlock.text / ReasoningBlock.text (plus a │ runtime `thinking` string field) - the deltas │ AND the block must agree, or consumers see │ inconsistencies ├─ tool-call-delta ───► ReplaceMap(argumentsDelta, Quotes) when │ normalizeToolArguments (IMPLEMENTED, default │ OFF - execution-critical raw JSON, the user's │ accepted risk; the example patch turns it │ on) - the assembled ToolCallBlock.arguments │ follows the same flag via block-end │ The gate is THREE-WAY with the flag on: a │ delta/block whose `name` is "edit" passes │ through BY IDENTITY (the edit tool's │ `old_string` must match the real file bytes), │ and a call whose arguments open with the │ `{"__normalize":false` marker (FIRST key, │ tracked per call id) passes through │ UNNORMALIZED with the marker entry stripped, │ so the executed call carries no unknown key └─ block-start / usage / finish ──► PASSTHROUGH BY IDENTITY, ALWAYS (usage/finish ordering is the adapter contract) │ count === 0 → original chunk BY IDENTITY; rewritten → shallow copy ▼ yield ──► downstream consumers = the live UI + the durable transcript │ (order preserved, no buffering; upstream throws propagate) ▼ normal loop completion, Count > 0 Factory.Append ──► `hook-dsh-normalize-quotes: normalized N quote char(s) in one stream`

The transform - exactly the core's Quotes map (@playform/hook-dsh-core's Normalize/Quotes), applied per text segment through the core's ReplaceMap - eight entries, each typographic quote code point to its ASCII straight counterpart:

Code pointGlyphCharacter (by name)ASCII
U+2018'left single quotation mark' (U+0027)
U+2019'right single quotation mark'
U+201A'single low-9 quotation mark'
U+201B'single high-reversed-9 quotation mark'
U+201C"left double quotation mark" (U+0022)
U+201D"right double quotation mark"
U+201E"double low-9 quotation mark"
U+201F"double high-reversed-9 quotation mark"

No context rules - one character in, its straight counterpart out. Chunk-boundary-safe: single-character substitution, no lookahead - per-chunk application can never disagree with whole-text application. The mapping values are returned through a function replacer, so they are inserted literally (no $-pattern interpretation). Replaced characters are counted per stream for the ledger line.

The Config

NO REPLACEMENT FIELD (MAP FLAVOR)
FieldTypeDefaultVolatileMeaning
logbooleantrueyeswrite the durable ledger file
logFilestring~/.dsh/hook-dsh-normalize-quotes.logyesthe quotes ledger (separate from the family's logs)
normalizeReasoningbooleantruenonormalize reasoning deltas and the assembled reasoning block too
normalizeToolArgumentsbooleanfalsenoIMPLEMENTED (default OFF): rewrite the tool-call argumentsDelta and the assembled ToolCallBlock.arguments when on (with the edit name exemption and the {"__normalize":false raw-marker pass-through) - execution-critical raw JSON, the user's accepted risk; the example patch turns it on

There is no replacement field (MAP flavor). Volatile cells commit without remounting the plugin; the factory's State builder unwraps them defensively. Example cordis.patch.yml row:

- insert: - id: hook-dsh-normalize-quotes name: "@playform/hook-dsh-normalize-quotes" config: log: true logFile: ~/.dsh/hook-dsh-normalize-quotes.log normalizeReasoning: true

In Action

ONE STREAM, ONE TRANSFORMATION

One stream, one transformation. The model emits smart-quoted prose; the live UI and the transcript receive the straight ASCII forms:

Before → after - the model stream, then what reaches the transcript

text-delta in (what the model wrote): ‘She said “let’s ship it” — today’s the day’ text-delta out (what reaches the transcript): 'She said "let's ship it" - today's the day'

Every typographic quote in the incoming text - the four single forms and the four double forms - becomes its ASCII counterpart; straight quotes that were already there are untouched (the count only counts replacements). In code blocks, commands and file paths this is the difference between a command that runs and one that fails for no visible reason. When a stream finishes normally with replacements made, the ledger gets the count line shown below. The same pass runs over reasoning deltas when normalizeReasoning is on, and over tool-call arguments when the example patch enables normalizeToolArguments - with the edit name exempt and the raw-marker calls passing through unnormalized.

The Ledger

TWO LINES VIA FACTORY APPEND

Two lines, both written through the factory's Append (the hook-dsh-normalize-quotes: prefix is the logger's <State.Module>:; the durable file line is [<ISO>] <message>):

hook-dsh-normalize-quotes: activated (reasoning=on, toolArgs=off, logFile=~/.dsh/hook-dsh-normalize-quotes.log) hook-dsh-normalize-quotes: normalized 8 quote char(s) in one stream

The activation line is written by apply(); the count line only follows a normal stream completion and only when N > 0 (a thrown-away stream writes no ledger line).

Related plugins

12 TOTAL
hook-dsh-core DSH FAMILY

Supplies the Quotes MAP, the ReplaceMap replacer and the generic Chunk/Block dispatch this flavor is a thin closure over.

The first stream normalizer sibling - and the registrar of the family-wide raw-write exemption.

plugin-dsh-factory PARENT SERVICE

The parent service: State, Append - plus the named Schema helper (shared: false) for the config.

License: MIT.