npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

v0.1.0

Published

The file-content normalizer flavor: a DeepSeek Harness plugin that registers the normalize family's `normalize-file` tool - the read → count → write pipeline over files ALREADY on disk, beyond the tool layer. One agent-chosen file per call: the target's c

Readme

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

The DeepSeek Harness Plugin Family for PlayForm.

npm release variant sibling license

[!NOTE]

The file-content normalizer - a DeepSeek Harness plugin that registers the normalize family's normalize-file TOOL: the read → count → write pipeline over files ALREADY on disk, beyond the tool layer.

One agent-chosen file per call: the target's content is read, the family's SIX transforms are applied with a per-character count, and only when N > 0 is the rewritten content written back through the factory's ONE shared write executor.

N = 0 writes NOTHING - the no-op no-write rule.

The family's first LISTENER-LESS flavor: no llm/stream, no fs/observed - nothing runs without an explicit agent action.

The background-rewrite posture is deliberately NOT shipped (it would break the edit tool's old_string contract); the design report ships the tool arm only.

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

This bundle's own tool calls are name-exempt in the stream gate beside edit and raw-write - its arguments carry a FILE PATH, and a normalized dash inside a filename would corrupt the target.


Where It Fits

Family position (the @-sentence Hook @ DSH @ Normalize @ File): a tool-child of the ets-plugin-dsh-factory service and the ets-hook-dsh-core machinery; the seventh sibling - the one flavor that works on files already on disk instead of model output.

One of seven siblings in the normalize family, all built on the shared machinery - ets-plugin-dsh-factory for State/Append/Journal/Write (and the named Schema helper, shared: false, for the config), ets-hook-dsh-core for the six transform tables and the generic Replace/ReplaceMap replacers:

| flavor | layer | mechanism | | ----------------------------------------------------------------- | -------------------------------- | -------------------------------------------------- | | ets-hook-dsh-normalize-dash | model output (stream) | core Dashes class -> replacement (default -) | | ets-hook-dsh-normalize-quotes | model output (stream) | core Quotes MAP -> curly -> straight | | ets-hook-dsh-normalize-ellipsis | model output (stream) | core Ellipsis class -> ... | | ets-hook-dsh-normalize-spaces | model output (stream) | core Spaces class -> " " | | ets-hook-dsh-normalize-invisible | model output (stream) | core Invisible class -> removed | | ets-hook-dsh-normalize-fullwidth | model output (stream) | core Fullwidth MAP -> full-width -> half-width | | ets-hook-dsh-normalize-file (this bundle) | files already on disk (tool) | all SIX, at write time, through Factory.Write |

The six stream flavors cover only what the model is emitting right now; the raw-write tool's normalize: true covers only content being written.

This flavor covers the gap in between: pre-existing files, git-cloned material, script-created files - anything already on disk that the agent did not just write.

The file-hook precedent is direct: the normalize-dashes-for-execute-code.sh hook swept script-created files after the fact; DSH makes the same rewrite an explicit, visible, opt-in TOOL call instead of a background hook (and normalize-tabs.sh - the repair hook for a repair hook - is the cautionary tale that keeps it that way).

A non-manifest factory consumer: it injects ["pluginFactory", "fs", "tools"] and uses State (cell unwrap + shared Ledger/Enabled mappings + its own field), Append (the ledger), Journal (the P5 storage record) and - uniquely in the family - Write, the ONE shared write executor.

The config is composed by the factory's standalone Schema helper with shared: false - the minimal block plus the two knobs.

The tool's writes carry the call's exec as the actor, so the governance trio's Gate (which pins mutationTools to write/edit/str_replace_editor) never treats them as a trigger - the same protection raw-write already has.

Install

Terminal (registry)

pnpm add @playform/ets-hook-dsh-normalize-file
  1. Remote / dependency install (auto-activation): pnpm add @playform/ets-hook-dsh-normalize-file 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 inside the bundle (its pnpm-workspace.yaml pins packages: [.] so the install lands in the bundle, not the workspace root) → build → dsh plugin --profile <name> add <this directory>.
  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.

Verify with the ets-hook-dsh-normalize-file: activated (...) ledger line.

Usage

The tool is called by the agent like any built-in tool - the bundle is configured through its cordis.patch.yml row (or the profile's dsh.bundle manifest), and the full config table is in The Config:

normalize-file { file_path: "notes/report.md" }
→ result { path, count, changed, before, after }   (the diff card shows
                                                     the before/after in
                                                     the same turn)

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

Files that never passed through a write tool are invisible to every normalization layer DSH has: raw-write's normalize: true is an explicit per-call opt-in for NEW writes, and the six stream flavors only rewrite model output - they never touch disk.

Left unnormalized, a pre-existing file keeps every typographic dash, curly quote, ellipsis, unicode space, zero-width character and full-width character it was born with - exactly the characters that break parsers, shells, diffs and byte-exact edit matches downstream.

But the fix cannot be another silent rewriter.

Files are rewritten behind the agent only at the price of the edit tool's old_string contract: the agent read bytes X, a background rewriter silently changed them to X', and the next edit fails or half-matches.

The lesson is real: normalize-tabs.sh, a repair hook for a repair hook, exists to repair the damage after-the-fact rewriting caused.

The tool is the answer: explicit, visible, discoverable, zero background activity.


How It Works

The pipeline

  agent calls normalize-file { file_path }
       |
       v
  ctx.fs.resolve                     the model-supplied path becomes a
       |                             stable target (caller-side, like raw-write)
       v
  ctx.fs.readText(target, signal)    the shared read path - a missing file,
       |                             a directory, a binary/undecodable target,
       |                             a permission failure or an abort rejects
       |                             here -> error result, NO write
       v
  Rewrite(content, replacement)      the counting six-fold chain, in order:
       |                               1. Dashes    class  -> replacement ("-")
       |                               2. Quotes    MAP    -> curly -> straight
       |                               3. Ellipsis  class  -> "..."
       |                               4. Spaces    class  -> " "
       |                               5. Invisible class  -> "" (removed)
       |                               6. Fullwidth MAP    -> full-width -> half-width
       |                             every step returns { text, count } and the
       |                             counts are summed -> N
       v
  N == 0 ?  -----------------------> NO write at all: the file is left
       |                               byte-identical, no ledger line, no
       |                               journal record; result: changed: false
       | N > 0
       v
  Factory.Write(state, target,       the ONE shared write executor:
       |    rewritten, { signal,       the fs/write-intent waterfall, the
       |    actor: exec,               standing sandbox policy, writeText end
       |    policy: "standing" })     to end, and the fs/observed
       |                             { kind: "present", version } emit ON THE
       |                             ROOT CONTEXT with the call's exec as the
       |                             actor - byte-identical with raw-write
       v
  ledger + journal                   `normalized N char(s) in <path>` via
                                      Factory.Append, and the `normalized`
                                      event of the shared package_governance
                                      v2 domain via Factory.Journal
       |
       v
  result { path, count, changed,     the diff card shows the outcome's
           before, after }           before/after in the same turn

The transformation is exactly the raw-write normalize: true chain - the core's tables and replacers, applied whole at write time.

There is no stream dispatch to gate: the tool registers NO event listener, so the six transforms are applied to the entire file content in one pass, and the count is the ledger's N and the no-op condition in one.

The conflict map, honored by construction

  • Governance bounded passes: the write goes through Factory.Write and emits fs/observed, but the governors' Gate requires the actor tool name in their mutationTools pin - normalize-file is never added to any such list, so these writes never trigger a chain pass.
  • Edit old_string contract: safe because visible - the diff card shows the before/after in the same turn, and the tool description says to re-read before editing (the same obligation as after any normalize: true write).
  • raw-write read-before-write: tool calls are serialized and Factory.Write's before is read at write time, so a prior normalize-file in the same turn is already reflected. No race.

The Pitfalls

  • The file's bytes change under you - re-read before editing: the edit tool's old_string must match the NEW content; the diff card shows the before/after in the same turn.
  • A no-op is a no-write - N = 0 leaves the file byte-identical: no write, no ledger line, no journal record; a missing, binary or undecodable target errors out the same way.
  • No governor triggers - the write emits fs/observed with the call's exec as the actor, but normalize-file is never in a governor's mutationTools, so no chain pass runs.
  • Name-exempt in the stream gate - the tool's arguments carry a file path and pass through the stream unnormalized, beside edit and raw-write.

The Config

| Field | Type | Default | Volatile | Meaning | | ------------- | ------- | ------------------------------------ | -------- | ----------------------------------------------------------- | | log | boolean | true | yes | write the durable ledger file | | logFile | string | ~/.dsh/ets-hook-dsh-normalize-file.log | yes | the normalize-file ledger (separate from the family's logs) | | replacement | string | "-" | yes | the dash step's replacement (the transform's only knob) |

There are no stream flags (normalizeReasoning, normalizeToolArguments) and no fs/observed fields (updateCooldownMs, mutationTools, policyFile, exclude): the minimal shared: false block plus the two knobs is the whole config surface, because the tool registers no listener of any kind.

Volatile cells commit without remounting the plugin; the factory's State builder unwraps them defensively.

The Granular Scopes (all volatile)

The scopes object gates every arm of the ets-hook-dsh-normalize-file hook independently — the defaults are exactly the pre-scopes behavior, and because the schema shape is identical in every family hook, one grouped override in the profile patch turns the whole family off in one edit (see the patch fragment at the end of this section). The whole object carries the volatile marker (its leaves stay plain — schemastery rejects volatile leaves under an enclosing volatile field): one leaf change commits one loader/volatile-update dispatch whose whole object the State's refresh listener re-reads — live, silent, no remount, no new activation line.

| Scope | Default | Gates | | ------------ | ------- | --------------------------------------------------------------------- | | conversation | on | text-delta text + the assembled text/reasoning blocks | | reasoning | on | the reasoning-delta sub-gate (ANDed with normalizeReasoning) | | toolInput | off | the tool-call arguments arm (OR-ed with normalizeToolArguments) | | toolResults | off | the tools/post-execute arm (tool results in the transcript) | | files | on | the normalize-file tool — off = the tool returns the unchanged changed:false result, no read, no write, no ledger line | | batch | off | the agent/pre-step arm (the incoming step batch) |

normalizeToolArguments is kept and OR-ed in (effective toolInput = the field || scopes.toolInput), so every existing profile keeps working.

The per-call {"__normalize":false first-key marker is the tool-args arm's escape hatch — the "leave that search alone" control: a deliberate search for an irregular character (an em dash, a curly quote, a full-width form as a PATTERN) survives byte-exact while the rest of the call stream normalizes. Documented edges: the marker must be the FIRST key, and a pathologically tiny first delta (shorter than the marker) is invisible to the startsWith probe — the call normalizes normally.

toolResultsExempt (volatile): a tool result whose exec name is listed passes the tool-results arm by identity — read, edit, raw-write, normalize-file, ccr_retrieve, subagent, ask_user_question by default.

The Overpass Arms

Two additional listeners, both registered at apply and both opt-in (gated at RUNTIME on the scope cells, so flipping a scope lands through the P10 volatile-refresh without any re-registration):

  • Tool results — tools/post-execute, { prepend: true } (OUTERMOST vs the ccr hook's own prepended gate: the ccr compression sees the NORMALIZED bytes; the <<<CCR: marker guard makes any load order safe). Accepted, non-value-carrying decisions only; TEXT blocks only (never tool-call/argument blocks); N = 0 is a true no-op — no ledger line; fail-open. The invisibility contract: the replacement is PRE-COMMIT — the waterfall settles before the result is appended to the durable log — so the model only ever sees the normalized output and never notices the replacement (the write-path counterpart is the governance trio's never-notify precedent: the tool result is built from the author's own content, and the observation-policy's version record is refreshed by the re-emitted fs/observed).
  • Step batch — agent/pre-step, default order. While scopes.batch is on every incoming batch is normalized; the command's one-shot arm fires ONCE regardless of the scope value and is cleared after one use. Text blocks only; never the assistant/tool history.

The file overpass lines (N > 0 only):

ets-hook-dsh-normalize-file: overpass: normalized N char(s) in tool result <toolName> <callId>
ets-hook-dsh-normalize-file: overpass: normalized N char(s) in step batch (turn T step S)
ets-hook-dsh-normalize-file: overpass: swept K file(s), normalized M char(s)

The normalize-overpass Command

Registered through the commands service when it is present (probe-once — a missing service simply skips the registration). Grammar:

| Input | Effect | | ------------------ | --------------------------------------------------------------------------------- | | files <path…> | run the file sweep — the normalize-file pipeline per path, each changed file written through the shared executor with its own count line, then the summary | | batch | arm the one-shot step-batch overpass (consumed by the next incoming batch) | | results on\|off | flip scopes.toolResults via the settings service / config editor — never direct State mutation; absent services → reports the settings-UI path | | scopes | read-only report of the current effective values (the activation line never changes) |

The Patch-Level Global Off

- id: ets-hook-dsh-normalize-file
  config:
    scopes:
      conversation: false
      reasoning: false
      toolInput: false
      toolResults: false
      files: false
      batch: false

In Action

One call, one file, one count.

The file already on disk carries the family's characters; the tool rewrites what changed and reports it:

before (what is on disk):

  Nova — “launch” ... Q3 — done

after (what the tool writes back):

  Nova - "launch" ... Q3 - done

Every em dash (U+2014) becomes the ASCII hyphen-minus, the curly quotes become straight ones, the ellipsis becomes three periods; five characters replaced, so N = 5 and the write happens.

The result carries the outcome and the diff card shows the before/after in the same turn; the ledger gets the count line:

ets-hook-dsh-normalize-file: normalized 5 char(s) in ~/Projects/notes/report.md

A file with nothing to replace is a no-op: changed: false, the file stays byte-identical, no write, no count line, no journal record.

A missing, binary or undecodable target is an error result with no write.

Because the file's bytes change under you, re-read before editing - the edit tool's old_string must match the new content.


The Ledger

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

ets-hook-dsh-normalize-file: activated (replacement=-, logFile=~/.dsh/ets-hook-dsh-normalize-file.log)
ets-hook-dsh-normalize-file: normalized N char(s) in <path>

The activation line is written by apply(); the count line follows only a successful N > 0 write - a no-op writes no line, and a failed read or an aborted call writes none either.

Each N > 0 write also journals one normalized record into the shared package_governance v2 domain (event normalized, path = the target's display 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.