DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Ets Hook Dsh Normalize Dash — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

@playform/ets-hook-dsh-normalize-dash

Ets Hook Dsh Normalize Dash

normalize-dash hook:一个 DeepSeek Harness 插件,挂接到 llm/stream waterfall——这是 EVERY 流式模型调用的可拦截封装器——并规范化 Unicode 破折号字符族(U+058A、U+05BE、U+1400、U+1806、U+2010–U+2015、U+2E17、U+2E1A、U

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add @playform/ets-hook-dsh-normalize-dash@0.0.1
README兼容性版本

兼容性与来源证明

Ets Hook Dsh Normalize Dash 以 @playform/ets-hook-dsh-normalize-dash 发布,当前版本为 0.0.1。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
npm
Registry 更新时间
2026/10/17

版本

0.0.1stable
2026/10/9
最新版
0.0.1
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
68.6 kB
文件数
29
Surface
any
许可证
CC0-1.0
发布源
npm
GitHub
★ 1
周下载
391
安全扫描
✓ v0.0.1 扫描通过
最近提交
2026/10/17
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

README

@playform/ets-hook-dsh-normalize-dash

The DeepSeek Harness Plugin Family for PlayForm.

[!NOTE]

The dash 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 unicode dash family to ASCII hyphen-minus, live in the transcript: em dashes, en dashes and seventeen exotic relatives → -.

The normalize-dashes hook, at the injection point the harness itself does not have.

A CLASS flavor of the normalize family: the core's Dashes table plus a configurable replacement (default -).

It also registers the family's raw-write tool - a write wrapper with an explicit normalize parameter (default false = verbatim), exempt from the stream normalization by name like edit.

The @-sentence identity: Hook @ DSH @ Normalize @ Dash.


Where It Fits

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

One of six siblings in the stream-normalization family, all built on the shared machinery - ets-plugin-dsh-factory for State/Append (and the named Schema helper, shared: false, for the config), ets-hook-dsh-core for the Dashes table and the generic Chunk/Block dispatch.

The normalize-dash is the family's CLASS flavor with a replacement knob - the core's Dashes class applied verbatim; the siblings share the same whole-chunk replacement approach:

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

It is also the factory's first NON-MANIFEST module and its second consumer: it injects ["pluginFactory"] and uses only State (cell unwrap + shared Ledger/Enabled mappings + its own fields) and Append.

NOT consumed, deliberately: Wire (fs/observed-hardwired - the family's silence; the normalize-dash implements its own llm/stream registration), Attach (no jobs / inflight / storage), Journal (the shared P5 record on every N > 0 pass), and the gate/write/refresh/continue machinery (no fs/observed path).

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.

The Interplay

  • The governance trio and the factory: pure additive - no shared event, no shared state. The STREAM listener touches no files, so fs/write-intent and fs/observed never see it; the raw-write TOOL does write files, and does so through the built-in write's exact fs path - resolve → fs/write-intent → writeText → fs/observed - so the governance hooks treat its writes exactly like built-in write calls.
  • Other llm/stream listeners: the normalize-dash wraps the downstream result and always calls next(), so it composes regardless of registration order; it neither short-circuits nor yields its own chunks.
  • Chunk identity: untouched chunks pass through by object identity; a rewritten delta/block is a shallow copy with only the text field replaced (the one exception: the raw-marker strip rewrites the arguments text while the count stays 0 - marker removal is not normalization)

Install

Terminal (registry)

pnpm add @playform/ets-hook-dsh-normalize-dash
  1. Remote / dependency install (auto-activation): pnpm add @playform/ets-hook-dsh-normalize-dash in a profile dir (+ the package name in dsh.profile.bundles, or dsh plugin add) - installed automatically and activated at the next host start.
  2. Local git clone: clone → pnpm install --ignore-workspace (the bundle carries its own node_modules) → build (npx Build 'Source/**/*.ts' --ESBuild Configuration/ESBuild.ts) → dsh plugin --profile <name> add <this directory>. The linked checkout must carry its own node_modules: pnpm install --ignore-workspace inside the bundle.
  3. Tarball / npm: dsh plugin --profile <name> add <tarball-or-spec> - the package ships built Target/ (minified, .d.ts twins), so no build runs at install time.
  4. Git: fetches sources, not builds - requires a prepare script and an allowBuilds entry, or a published tarball.

Activation is at boot (bundle layers compose at host restart); verify with the ets-hook-dsh-normalize-dash: activated (...) ledger line (the --dump-config verification path is gone behind the app-managed profile guard - the ledger and the runtime's inspect providers are the activation assessors).

A file: dependency alone is NOT activation.

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: ets-hook-dsh-normalize-dash
        name: "@playform/ets-hook-dsh-normalize-dash"
        config:
            log: true
            logFile: ~/.dsh/ets-hook-dsh-normalize-dash.log
            replacement: "-"
            normalizeReasoning: true

The shared Effect-TS plumbing - the runtime wiring, the stream machinery and the hook contract types - comes from the base ets-base-dsh package.


The Problem

The established file-hook approach rewrites the unicode dash family to ASCII hyphen-minus in files after the fact - perl -CSD -pe over whatever was already written.

Model output, however, is born in the stream: the live UI and the durable transcript see the em dashes first, no matter what a file hook does later.

A DSH plugin can go where a file hook cannot: the llm/stream waterfall, where every streaming model call passes through.

Unlike the governance family (ets-hook-dsh-package-governor, ets-hook-dsh-package-pinner, ets-hook-dsh-cargo-governor), whose rewrites are SILENT, this plugin's normalization is deliberately VISIBLE - it IS the feature: the rewrite flows into both the live UI and the durable 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 (Function/Normalize)
       │   for await (chunk of upstream)
       ▼
  CoreChunk(chunk, transform, reasoning, toolArgs, raw)
       │                                   the core's per-chunk dispatch
       │
       ├─ text-delta ────────► Replace(text, Dashes, "-")   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 ───► Replace(argumentsDelta, Dashes, "-") 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" or
       │                       "raw-write" passes through BY IDENTITY (the
       │                       edit tool's `old_string` must match the real
       │                       file bytes; the raw-write tool owns its
       │                       normalization through the explicit `normalize`
       │                       parameter), 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 ──► `ets-hook-dsh-normalize-dash: normalized N dash char(s) in one stream`

Waterfall discipline:

  • next() is ALWAYS called, unconditionally, first - omitting it short-circuits the waterfall and the model call never happens.
  • options is NEVER touched: a LOOP-built request arrives deep-frozen (mutation throws) and its content is a pure function of the session log - listeners read it, never rewrite it.
  • Order preserved, no buffering: one chunk in, one chunk out; upstream throws propagate (the LlmRuntime normalizes adapter failures to a terminal error/aborted finish - a thrown-away stream writes no ledger line).

The transform

The core's Dashes class, verbatim - the same perl pattern (perl -CSD -pe), applied per text segment:

[\u058A\u05BE\u1400\u1806\u2010-\u2015\u2E17\u2E1A\u2E3A-\u2E3B\u2E40
 \u2E5D\u301C\u3030\u30A0\uFE31-\uFE32\uFE58\uFE63\uFF0D]  →  replacement

Armenian hyphen, Hebrew maqaf, Canadian syllabics hyphen, Mongolian todo soft hyphen, the U+2010 - U+2015 hyphen/dash family (including the em dash and the non-breaking hyphen), double oblique hyphen, hyphen with diaeresis, two-/ three-em dash, double hyphen, U+2E5D, wave/wavy dash, katakana-hiragana double hyphen, the vertical presentation dashes, small em dash, small hyphen-minus and the fullwidth hyphen-minus - all become the config replacement (default -, ASCII hyphen-minus U+002D).

  • No context rules - a class match and replace, no lookahead, no context.
  • Chunk-boundary-safe: single-character replacement, no lookahead, no multi-character sequences - per-chunk application can never disagree with whole-text application.
  • The replacement is applied with a function replacer, so a custom replacement containing $ patterns is inserted literally.
  • Replaced characters are counted per stream for the ledger line.

The raw-write tool

Besides the stream listener, the normalize-dash registers the family's raw-write tool (Source/Function/Write.ts): a plugin-registered write wrapper that wraps the built-in write operation with an explicit normalize parameter - the tool is exempt from the stream normalization by name (the core's Stream/Chunk + Stream/Block pass every raw-write call through by identity, exactly like edit), so the content is what the model sent, and the normalization decision happens at execution time:

normalizewhat the exec writes
absent or false (default)the content VERBATIM, byte-for-byte
true or "all"the family's SIX transforms applied in order, daisy-chaining the text: Dashes (→ the module's replacement), Quotes (curly → straight), Ellipsis (U+2026 → ...), Spaces (unicode spaces → " "), Invisible (removed), Fullwidth (full-width → half-width)
["dash", ...]ONLY the selected flavors, applied in the family's fixed order (dashes → quotes → ellipsis → spaces → invisible → fullwidth); the parameter's schema declares the union (`boolean

The tool also takes a govern parameter - the per-call governance selection over the factory's direct-govern registry (factory SCHEME.md §2.16–§2.17), applied after a successful write:

governwhat the exec runs
absent or false (default)NO governance chain — the current posture, the "skip everything" escape hatch (the write still emits fs/observed exactly like a built-in write, so the event-path hooks behave as always)
true or "all"ALL registered governance steps for the target's basename (package.json: canonicalize + update; Cargo.toml: cargo + update)
["canonicalize", ...]ONLY the named steps (canonicalize, pin, cargo, update); unknown names are rejected by the schema and ignored by the registry

Governance is best-effort and contained: the steps run through the factory's Govern(target, selection, actor, version) with the written outcome's fresh version — a failing step never affects the write's outcome, and an empty registry makes the call a contained no-op.

The exec mirrors the built-in write's fs path exactly - ctx.fs.resolve → the fs/write-intent waterfall (the observation policy's single-slot guarded-write decision) → the standing sandbox policy (resolved like the built-in's no-escalation path; raw-write advertises no escalation of its own) → ctx.fs.writeText(target, content, intent, exec.signal, policy) → the fs/observed emit with the written version - so the governance family's fs/observed chain fires for these writes the same way it fires for built-in writes.

exec.signal is honored end to end (a pre-aborted call never writes).

The registration is readonly after registration; the presenters are pure and recompute the same selection-aware chain, so the diff card always matches the file.

Inject: ["pluginFactory", "fs", "tools"].

The Source Layout

Source/Library.ts            - the loader contract (name/apply/Config/inject + default object)
Source/Function/Apply.ts     - thin wire-up: Factory.State → activation proof → ctx.on("llm/stream")
                               → ctx.tools.register(raw-write)
Source/Function/Normalize.ts - the async-generator stream wrapper + the per-stream count line
Source/Function/Chunk.ts     - the thin per-chunk dispatch (delegates to the core's generic dispatch)
Source/Function/Replace.ts   - the thin transform leaf (the core's Replace + Dashes, pinned)
Source/Function/Write.ts     - the raw-write tool definition (built, registered in Apply; never
                               mutated after registration)
Source/Function/Rewrite.ts   - the tool's selection-aware normalize step (the core's tables,
                               chained; the family's fixed order, ONLY the selected flavors)
Source/Variable/Config.ts    - the module's own Schemastery schema (the factory's standalone
                               helper with `shared: false` - the minimal block)
Source/Interface/*.ts        - Config/State shapes, the factory service view (chunk vocabulary
                               imported directly from @deepseek-ai/dsh-llm, type-only; the fs
                               vocabulary arrives through the factory's own declarations)

The Pitfalls

  • The stream gate rewrites model output only - it never touches disk; files already on disk are the normalize-file tool's job (or raw-write's explicit normalize parameter).
  • The count is per replaced code point - N counts matched characters, not output characters; N = 0 and an upstream throw both write no count line and no journal record.
  • One class, one table - the em/en dash family is this flavor's class; the quotes map never touches a dash (and this table never touches a quote) - run the flavors the session's characters call for.
  • The replacement is a literal string - a $ pattern in replacement is never interpreted (the function replacer inserts it verbatim).

The Config

FieldTypeDefaultVolatileMeaning
logbooleantrueyeswrite the durable ledger file
logFilestring~/.dsh/ets-hook-dsh-normalize-dash.logyesthe normalize-dash ledger (separate from the family's logs)
replacementstring-yesthe transform's only knob - hot-editable, the next stream picks it up with no remount
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 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

Volatile cells commit without remounting the plugin (the fiber - and with it the llm/stream registration - stays alive); the factory's State builder unwraps them defensively.


In Action

One stream, one transformation.

The model emits prose with typographic dashes; the live UI and the transcript receive the ASCII forms:

text-delta in (what the model wrote):

  "The refactor is complete — every call site updated, ranges 10–15
   covered, and the odd U+2015 bar ― swept too."

text-delta out (what reaches the transcript):

  "The refactor is complete - every call site updated, ranges 10-15
   covered, and the odd U+2015 bar - swept too."

The em dash (U+2014), the en dash (U+2013) and the horizontal bar (U+2015) each become -; the ASCII hyphens that were already there are untouched (count only counts replacements).

The same pass runs over reasoning deltas (normalizeReasoning, default on) and - with the example patch enabling normalizeToolArguments - over tool-call arguments, where the three-way gate keeps the sensitive calls intact:

tool-call-delta, name "edit"        → passes through BY IDENTITY
                                      (old_string must match real file bytes,
                                       em dashes and all)
tool-call-delta, name "raw-write"   → passes through BY IDENTITY
                                      (the tool owns its normalization
                                       through its explicit `normalize`
                                       parameter)
tool-call-delta, name "write",
  arguments `{"file": "a — b.txt"}` → normalized to `{"file": "a - b.txt"}`

And the raw-write tool in the file-writing direction: its content lands verbatim by default, or runs the family's six-fold chain when called with normalize: true - the em dash above would survive the default call and become a hyphen under normalize: true.

When a stream finishes normally with replacements made, the ledger gets the count line:

ets-hook-dsh-normalize-dash: normalized 4 dash char(s) in one stream

The selection can also be FINE-GRAINED - the same call that wrote the prose above, done with the raw-write tool per call.

The literal characters in these examples are written with the RAW-WRITE tool itself (this is exactly the stream-exempt path the tool exists for):

raw-write, content "The refactor is complete — every call site updated, ranges 10–15 covered, and the odd U+2015 bar ― swept too.", normalize absent:

→ the file matches the input byte-for-byte (em dash, en dash and the horizontal bar all survive)

raw-write, same content, normalize: ["dash"]:

→ The refactor is complete - every call site updated, ranges 10-15 covered, and the odd U+2015 bar - swept too. (ONLY the dash-family characters are replaced; a curly quote, an ellipsis or a full-width character in the same content would survive untouched)

raw-write, content "He said “wait” … then left.", normalize: ["quotes"]:

→ He said "wait" … then left. (ONLY the curly quotes are replaced — the ellipsis and the full-width period are untouched by the quotes-only selection)

raw-write, content "He said “wait” … then left.", normalize: true:

→ He said "wait" ... then left. (all six transforms, in the family's fixed order — the legacy behavior)

The order is FIXED by the family (dashes → quotes → ellipsis → spaces → invisible → fullwidth) even when the selection lists the flavors out of order: normalize: ["fullwidth", "dash"] still applies dashes first.

And the governance direction, per call: a raw-write to a package.json with govern: true runs the registered chain pass (canonicalize) and the update stage through the factory's direct-govern registry after the write lands; govern: ["pin"] runs only the pinner's chain pass; govern: ["update"] runs only the update stage.

The default — govern absent — writes the file and lets the fs/observed event path govern it exactly as before:

raw-write  package.json, govern: ["canonicalize"]
  → the write completes verbatim (or normalized, per `normalize`)
  → then the governor's chain pass runs: canonicalizes the chain-governed
    pins (ledger: `governed <path> → <version>`) — best-effort, contained,
    never affecting the write's outcome

raw-write  Cargo.toml, govern: ["cargo", "update"]
  → then the cargo module's chain + normalization pass and its update stage
    run, in the registered order

The Ledger

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

ets-hook-dsh-normalize-dash: activated (replacement=-, reasoning=on, toolArgs=off, logFile=~/.dsh/ets-hook-dsh-normalize-dash.log)
ets-hook-dsh-normalize-dash: normalized N dash 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.


License 📜

CC0-1.0.