Supplies the Quotes MAP, the ReplaceMap replacer and the generic Chunk/Block dispatch this flavor is a thin closure over.
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.
$ pnpm add @playform/hook-dsh-normalize-quotes 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 (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:
| Flavor | Table | Substitution |
|---|---|---|
| hook-dsh-normalize-dash | core Dashes class | → replacement (default -) |
| hook-dsh-normalize-quotes (this bundle) | core Quotes MAP | curly → straight |
| hook-dsh-normalize-ellipsis | core Ellipsis class | U+2026 → ... |
| hook-dsh-normalize-spaces | core Spaces class | unicode spaces → " " |
| hook-dsh-normalize-invisible | core Invisible class | removed (default "") |
| hook-dsh-normalize-fullwidth | core Fullwidth MAP | full-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
| Seam | What the plugin does there | What you can observe |
|---|---|---|
| llm/stream - the model stream waterfall | The 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 service | A 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 / session | Two 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
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 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 point | Glyph | Character (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
| Field | Type | Default | Volatile | Meaning |
|---|---|---|---|---|
| log | boolean | true | yes | write the durable ledger file |
| logFile | string | ~/.dsh/hook-dsh-normalize-quotes.log | yes | the quotes ledger (separate from the family's logs) |
| normalizeReasoning | boolean | true | no | normalize reasoning deltas and the assembled reasoning block too |
| normalizeToolArguments | boolean | false | no | IMPLEMENTED (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:
In Action
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
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, 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>):
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
The first stream normalizer sibling - and the registrar of the family-wide raw-write exemption.
The parent service: State, Append - plus the named Schema helper (shared: false) for the config.
License: MIT.