PLUGIN DETAIL FLAVOR: FULLWIDTH MAP FLAVOR

hook-dsh-normalize-fullwidth

Hook @ DSH @ Normalize @ Fullwidth • _The DeepSeek Harness Plugin Family for PlayForm._ The fullwidth 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 full-width family in model output, live in the transcript: the entire FULLWIDTH FORMS range U+FF01-U+FF5E maps to its ASCII half-width counterpart U+0021-U+007E - the whole story, letters, punctuation and digits included. A MAP flavor of the normalize family: the core's Fullwidth 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-fullwidth
Namespace: @playform/hook-dsh-normalize-fullwidth Release: v0.0.1
Archetype: Hook Event: llm/stream Table: core Fullwidth 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: 6TH OF 6 STREAM NORMALIZERS

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

FlavorTableSubstitution
hook-dsh-normalize-dashcore Dashes class→ replacement (default -)
hook-dsh-normalize-quotescore 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-fullwidth (this bundle)core 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.Half-width ASCII reaches 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 FULLWIDTH FLAVOR EXISTS

CJK input methods emit full-width punctuation, digits and letters - and a full-width comma or digit in a command, code block or path is invisible on screen and wrong for every parser, shell and diff. This flavor maps the whole fullwidth range to the unambiguous ASCII half-width forms before it 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, Fullwidth) 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, Fullwidth) 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-fullwidth: normalized N fullwidth char(s) in one stream`

The transform - exactly the core's Fullwidth map (@playform/hook-dsh-core's Normalize/Fullwidth), applied per text segment through the core's ReplaceMap - the standard fullwidth-to-halfwidth mapping over the whole range:

Code pointContentsASCII
U+FF01-U+FF0Ffull-width punctuation (exclamation through solidus)!"#$%&'()*+,-./
U+FF10-U+FF19full-width digits zero through nine0123456789
U+FF1A-U+FF20full-width punctuation (colon through commercial at):;<=>?@
U+FF21-U+FF3Afull-width capitals A through ZA-Z
U+FF3B-U+FF40full-width punctuation (left square bracket through grave)[\]^_`
U+FF41-U+FF5Afull-width small letters a through za-z
U+FF5B-U+FF5Efull-width punctuation (left curly brace through tilde){|}~

No context rules - one character in, its half-width 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 - the table's backslash, circumflex, vertical bar and tilde values are exactly the characters a string replacement would corrupt). 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-fullwidth.logyesthe fullwidth 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-fullwidth name: "@playform/hook-dsh-normalize-fullwidth" config: log: true logFile: ~/.dsh/hook-dsh-normalize-fullwidth.log normalizeReasoning: true

In Action

ONE STREAM, ONE TRANSFORMATION

One stream, one transformation. The model emits CJK-typing artifacts - a full-width word, a full-width path punctuation run, a full-width digit; the live UI and the transcript receive the half-width ASCII forms:

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

text-delta in (what the model wrote): path "/usr/local/bin" — version 123 — done! text-delta out (what reaches the transcript): path "/usr/local/bin" - version 123 - done!

Every full-width code point in the incoming text - the solidus U+FF0F, the letters U+FF55-U+FF4E, the digits U+FF11-U+FF13 and the exclamation U+FF01 - becomes its U+0021-U+007E half-width counterpart; ASCII characters that were already half-width are untouched. This is what keeps a pasted path openable and a pasted number parseable. The backslash, circumflex, vertical bar and tilde entries are exactly the characters a naive string replacement would corrupt - hence the function replacer. 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 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-fullwidth: prefix is the logger's <State.Module>:; the durable file line is [<ISO>] <message>):

hook-dsh-normalize-fullwidth: activated (reasoning=on, toolArgs=off, logFile=~/.dsh/hook-dsh-normalize-fullwidth.log) hook-dsh-normalize-fullwidth: normalized 4 fullwidth 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 Fullwidth MAP, the ReplaceMap replacer and the generic Chunk/Block dispatch this flavor is a thin closure over.

plugin-dsh-factory PARENT SERVICE

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

License: MIT.