hook-dsh-normalize-quotes
Verified@playform/hook-dsh-normalize-quotes · v0.0.1 · CC0-1.0
The quotes hook: a DeepSeek Harness plugin that hooks the llm/stream waterfall — the interceptable wrapper around EVERY streaming model call — and normalizes the curly quote family (U+2018 left single quotation mark, U+2019 right single quotation mark, U+
Install
dsh plugin add @playform/hook-dsh-normalize-quotes Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Readme
@playform/hook-dsh-normalize-quotes
The DeepSeek Harness Plugin Family for PlayForm.
[!NOTE]
The quote normalizer for model output - a DeepSeek Harness plugin that hooks the
llm/streamwaterfall (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
Quoteschar-to-char table owns the substitution - noreplacementconfig knob.The @-sentence identity: Hook @ DSH @ Normalize @ Quotes.
The family's
raw-writetool (registered byhook-dsh-normalize-dash) bypasses this flavor's transforms too - the exemption is family-wide: the core'sStream/Chunk+Stream/Blockpass everyraw-writecall through by identity, so the tool's explicitnormalizeparameter (default false = verbatim) is the only normalization it applies.
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.
One of six siblings in the stream-normalization family, all built on the shared machinery -
plugin-dsh-factory for State/Append (and the named Schema helper,
shared: false, for the config), hook-dsh-core for the Quotes table and the
generic ReplaceMap/Chunk/Block dispatch:
| 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 State (cell
unwrap + shared Ledger/Enabled mappings + its own fields), Append and
Journal (the shared P5 record on every N > 0 pass); 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.
The DSH plugin family is the DeepSeek Harness plugin layer of the PlayForm ecosystem:
TypeScript-first Source/ → Target/, the deterministic @playform build, prepublishOnly-only -
the same conventions as every other @playform package.
Install
Terminal (registry)
pnpm add @playform/hook-dsh-normalize-quotes
- Remote / dependency install (auto-activation):
pnpm add @playform/hook-dsh-normalize-quotesin a profile dir (+ the package name indsh.profile.bundles, ordsh plugin add) - installed automatically and activated at the next host start. - Local git clone: clone →
pnpm installinside the bundle (itspnpm-workspace.yamlpinspackages: [.]so the install lands in the bundle, not the workspace root) → build →dsh plugin --profile <name> add <this directory>. - Tarball / npm:
dsh plugin --profile <name> add <tarball-or-spec>- the package ships builtTarget/(minified,.d.tstwins), so no build runs at install time.
Verify with the hook-dsh-normalize-quotes: activated (...) ledger line.
Usage
The bundle is configured through its cordis.patch.yml row (or the profile's dsh.bundle
manifest) - the full config table is in The Config:
- insert:
- id: hook-dsh-normalize-quotes
name: "@playform/hook-dsh-normalize-quotes"
config:
log: true
logFile: ~/.dsh/hook-dsh-normalize-quotes.log
normalizeReasoning: true
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 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", "raw-write"
│ or "normalize-file" passes through BY IDENTITY
│ (the edit tool's `old_string` must match the real
│ file bytes; the raw-write tool owns its content
│ normalization; the normalize-file tool's arguments
│ carry a file path - a normalized dash inside a
│ filename would corrupt the target),
│ 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 point | 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 Pitfalls
- A dependency alone is NOT activation - the package must be in
dsh.profile.bundles(or the patch row inserted), or no listener registers and no ledger exists. - The map is quote-only - the em dash U+2014 and the rest of the dash family are the dashes flavor's class; a quotes-only pass leaves them untouched - run the flavors the session's characters call for.
- The stream gate rewrites model output only - it never touches disk; files already on disk are
the
normalize-filetool's job. - The count is per replaced code point - N counts matched characters; N = 0 and an upstream throw both write no count line and no journal record.
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 three exemptions: edit, raw-write and normalize-file calls pass through by identity (the edit tool's old_string must match the real file bytes; the raw-write tool owns its content normalization through its explicit normalize parameter; the normalize-file tool's arguments carry a file path - a normalized dash inside a filename would corrupt the target); and a call whose arguments open with the {"__normalize":false first-key marker passes through unnormalized, the marker entry stripped) - 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.
In Action
One stream, one transformation.
The model emits smart-quoted prose; the live UI and the transcript receive the straight ASCII forms:
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). The sample carries six of the eight forms; the em dash U+2014 is the dashes flavor's character, not the quotes map's - with only the quotes flavor it passes through untouched.
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:
hook-dsh-normalize-quotes: normalized 6 quote char(s) in one stream
(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, raw-write and normalize-file names 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>):
hook-dsh-normalize-quotes: activated (reasoning=on, toolArgs=off, logFile=~/.dsh/hook-dsh-normalize-quotes.log)
hook-dsh-normalize-quotes: normalized N 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).
Every N > 0 pass also journals one normalized record into the shared package_governance v2
domain - event normalized, path = the empty stream-level path, detail byte-identical to the
count line - best-effort: with no storage facility the record buffers or drops, and the human
ledger stays the complete record.
The two release groups (CLASSIC vs EFFECT-TS)
Two published groups, one contract: this package is the CLASSIC build - plain TypeScript,
zero framework dependencies: import ... from "@playform/hook-dsh-normalize-quotes" resolves to Target/.
The EFFECT-TS implementation of the same contract is the separate package @playform/ets-hook-dsh-normalize-quotes -
effect-backed, built on the shared @playform/ets-base-dsh base with effect v4.0.2 as
its runtime dependency. Install the group you run and never mix the groups in one graph
(the effect-ts Update returns Effect envelopes, not the classic dispatch envelope).
License 📜
CC0-1.0.