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-dash

v0.1.0

Published

The normalize-dash hook: a DeepSeek Harness plugin that hooks the llm/stream waterfall — the interceptable wrapper around EVERY streaming model call — and normalizes the unicode dash family (U+058A, U+05BE, U+1400, U+1806, U+2010–U+2015, U+2E17, U+2E1A, U

Readme

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

The DeepSeek Harness Plugin Family for PlayForm.

npm release variant sibling license

[!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:

| flavor | table | substitution | | ----------------------------------------------------------------- | ---------------------- | ----------------------------- | | ets-hook-dsh-normalize-dash (this bundle) | core Dashes class | → replacement (default -) | | ets-hook-dsh-normalize-quotes | core Quotes MAP | curly → straight | | ets-hook-dsh-normalize-ellipsis | core Ellipsis class | U+2026 → ... | | ets-hook-dsh-normalize-spaces | core Spaces class | unicode spaces → " " | | ets-hook-dsh-normalize-invisible | core Invisible class | removed (default "") | | ets-hook-dsh-normalize-fullwidth | core Fullwidth MAP | full-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:

| normalize | what 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 \| "all" \| the flavor-name array), because the harness tools reject unknown keys |

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:

| govern | what 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

| Field | Type | Default | Volatile | Meaning | | ------------------------ | ------- | ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | log | boolean | true | yes | write the durable ledger file | | logFile | string | ~/.dsh/ets-hook-dsh-normalize-dash.log | yes | the normalize-dash ledger (separate from the family's logs) | | replacement | string | - | yes | the transform's only knob - hot-editable, the next stream picks it up with no remount | | 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 |

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.

The Granular Scopes (all volatile)

The scopes object gates every arm of the ets-hook-dsh-normalize-dash 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 | inert in a stream flavor (present so one patch override drives the whole family) | | 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 dash overpass lines (N > 0 only):

ets-hook-dsh-normalize-dash: overpass: normalized N char(s) in tool result <toolName> <callId>
ets-hook-dsh-normalize-dash: overpass: normalized N char(s) in step batch (turn T step S)
ets-hook-dsh-normalize-dash: 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…> | declined — the sweep is owned by the normalize-file hook | | 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-dash
  config:
    scopes:
      conversation: false
      reasoning: false
      toolInput: false
      toolResults: false
      files: false
      batch: false

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.