dsh-word-lookup
Verifieddsh-word-lookup · v0.1.0 · MIT · Web UI
Local English-Chinese dictionary plugin for DeepSeek Harness (DSH).
Install
dsh plugin add dsh-word-lookup Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-word-lookup
A low-interruption English–Chinese dictionary for the DSH Web client. Select an English word or phrase in a conversation and look it up with a keyboard shortcut; nothing leaves your machine and no model is involved.
Status
v0.1.0 release source. The plugin is local-first and uses SQLite for dictionary lookup. The public package does not redistribute the full ECDICT corpus; production-corpus activation is an explicit local opt-in.
| Area | State |
|---|---|
| Target client | DSH Web (0.2.0-rc.2) |
| DSH baseline | 0.2.0-rc.2, Web only — the desktop client is not a tested target |
| Package version | 0.1.0 |
| Manual shortcut lookup | works; primary supported path |
| Settings persistence | works, verified across a full restart |
| Automatic lookup on double-click | works when autoDoubleClick is on (default off), verified in a real browser |
| Automatic lookup on drag-select | works when autoSelection is on (default off); portability outside the measured environment remains a known release limitation |
| Dictionary storage | local SQLite (node:sqlite), package-owned deterministic fixture by default |
| Dictionary data (ECDICT) | local ECDICT corpus supported via explicit DSH_WORD_LOOKUP_DB_PATH opt-in; full corpus is not bundled |
| Dictionary card UI | headword, phonetic, POS, Chinese meaning, forms and examples |
| Pointer devices | mouse only — automatic lookup is refused for pen, touch and an unidentifiable pointer |
Install
dsh plugin --profile web add [email protected]
dsh --profile web
The package intentionally does not include the full ECDICT corpus. Without DSH_WORD_LOOKUP_DB_PATH, the deterministic fixture dictionary is used. To use an already-built and provenance-compatible production database, set the environment variable before starting DSH:
$env:DSH_WORD_LOOKUP_DB_PATH = "E:\\path\\to\\ecdict.db"
dsh --profile web
If explicit production activation fails schema, metadata, hash, or integrity validation, the plugin fails closed and does not fall back to the fixture.
Known v0.1.0 limitation
autoSelection remains opt-in and defaults to off. Its portability across unmeasured operating systems, display servers, DPI settings, and platform multi-click configurations is not yet established. The manual Primary+Shift+L path is unaffected and remains the primary supported interaction.
The two automatic switches (autoDoubleClick, autoSelection) exist in the
settings UI, default to off, and persist. Each one drives exactly the
gesture it names:
autoSelection a completed pointer drag selection (not a selectionchange)
autoDoubleClick a double click the browser recognised
Neither switch affects the manual Primary+Shift+L shortcut, which is the only
path that reaches the dictionary with both switches off.
A double click is never answered by autoSelection. The classifier's 5 px
drag threshold and the browser's own double-click recognition are different
rules, so a second press that drifts while selecting a word can satisfy both —
and the browser still reports the whole thing as dblclick. The plugin reads the
platform's click multiplicity off the mousedown that opens each press and
refuses an auto-selection lookup unless the press was verifiably a single
click, so a gesture the platform calls a double click is answered by
autoDoubleClick and nothing else. A press whose multiplicity was never observed
is refused too: an unmeasured press does not become automatic I/O.
autoSelection is not "the selection changed." A keyboard selection, a
programmatic one and the selection a double click produces are all deliberately
outside it, and the plugin never issues a lookup from a selectionchange event.
That is what keeps one double click to one lookup instead of two, and it is
asserted two ways: a syntax-tree check that the selectionchange handler reaches
no I/O, and a browser measurement that fifty programmatic selections issue zero
requests with autoSelection on.
How it works
browser (client half) host (Node half)
selectionchange -> local snapshot only
pointer/dblclick -> local gesture state only
+ monotonic gesture identity
gesture completes --> trigger gate --> maybe a lookup
shortcut run -------> POST api/dsh-word-lookup ----> SQLite dictionary lookup
(fixture by default;
ECDICT when explicitly activated)
<--------- structured result
latest request wins <--------- card state
shell.overlay <---- renders the card
- No AI. The lookup path never calls a model provider, a translation API or anything that consumes tokens. There is no network call other than the local route back to the host process.
- No transcript mutation. The plugin only reads
SelectionandRange. It never inserts elements into conversation text and never wraps a selection. - Interface only through DSH surfaces. The overlay is a
shell.overlayoccupant, the command is registered with DSH's shortcut service, and the settings use DSH's own configuration form so the switches persist with the profile.
Dictionary Storage & Runtime Activation Semantics
The plugin operates with fail-closed dictionary loading semantics:
- Default Fixture Mode (Unset
DSH_WORD_LOOKUP_DB_PATH): WhenDSH_WORD_LOOKUP_DB_PATHis not set, the plugin initializes its package-owned deterministic fixture database (dsh-word-lookup-fixture). This is the default public-package behavior and is intentionally distinct from production-corpus activation. - Explicit Production Corpus Opt-In (
DSH_WORD_LOOKUP_DB_PATH): SettingDSH_WORD_LOOKUP_DB_PATH=<path-to-db>explicitly requests production dictionary activation. The host loads the specified SQLite database in read-only mode and strictly verifies the production schema, indexes (idx_forms_headword_raw,idx_examples_headword), primary keys, and metadata (corpus_name === 'ECDICT',upstream_commit,source_sha256,schema_version) against the pinnedcorpus/ecdict.manifest.json. - Fail-Closed Guarantee (No Silent Fallback):
If production activation is requested via
DSH_WORD_LOOKUP_DB_PATHand the database is missing, corrupted, or fails metadata/schema verification:- The plugin refuses activation with a controlled
DictionaryUnavailableError. - The lookup route is NOT registered (answering HTTP 404).
- No silent fallback to fixture database occurs.
- No fallback to AI/remote dictionary occurs.
- The plugin refuses activation with a controlled
When a lookup happens
Three paths, and only three:
| path | trigger | can it be disabled |
|---|---|---|
shortcut |
Primary+Shift+L on a qualifying selection |
no — it is the first-class path |
auto-selection |
a completed pointer drag that selected qualifying text | yes, autoSelection |
auto-double-click |
the browser's own dblclick on qualifying text |
yes, autoDoubleClick |
Every automatic lookup passes through one gate (src/client/trigger.ts) which
decides, in order: is this a real drag or double click; has this gesture identity
already been used; is the switch that owns it on right now; is the pointer a
kind this build measured; for a drag, was the press the platform's own single
click; and was the selection captured when the gesture completed eligible. A
refusal never consumes the gesture, so turning a switch on makes the very next
gesture work and nothing else.
Identity is a monotonic number allocated when a pointer press opens a gesture —
never the text, never the rectangle, never a time window. Two deliberate
double-clicks on the same word are therefore two lookups, and one gesture can
never buy two. Identity alone was not enough to keep the two switches apart: a
drifting double click spent its identity on the drag path before the browser's
dblclick arrived, which is why the platform's click multiplicity is carried
with the classification and checked by the gate.
Requests are numbered too, so the newest lookup owns the card: a slow answer for A cannot roll the card back from B, and a superseded failure cannot bury a newer hit.
The dictionary
src/host/fixture.ts holds the fixture as literals; scripts/build-fixture-db.mjs
materialises it into fixtures/dictionary.fixture.db; src/host/sqlite-dictionary.ts
answers from it through prepared statements with bound parameters. The host opens
the database once per plugin lifecycle and closes it on unload, so repeated
load/unload cycles cannot accumulate handles.
The fixture database path is package-derived and is not user-configurable. Production-corpus activation is intentionally separate: an operator may provide a read-only compatible database path through the process environment variable DSH_WORD_LOOKUP_DB_PATH. There is no writable UI dictionaryPath setting.
npm run build:fixture # rebuild fixtures/dictionary.fixture.db and validate it
npm run verify runs that step first, so the runtime always reads a freshly
validated file. The database is generated rather than committed: every row is a
literal in src/host/fixture.ts, and the host rebuilds it on first open if it is
absent or stale.
Lookup precedence is fixed: an exact entry (including an exact multi-word phrase)
first, then the forms table for an inflection, then that headword's examples.
There is no stemming, no spelling correction, no lemma guessing, and never a
silent split of a phrase into separate word queries.
Commands
npm install
npm run verify # fixture + typecheck + unit tests + build + static bundle checks
Individual steps:
npm run typecheck
npm test
npm run build
Testing against a real DSH instance
Never test against the DSH you are using. A test once ran against a live profile and destroyed it. Runtime tests must run in a fully isolated environment, and the only supported entry point is:
npm run iso:check # verify the target is isolated and the port is free
npm run test-profile:create # build the isolated profile
npm run test:runtime # boot it, measure, shut it down
npm run test-profile:cleanup -- --evidence-recorded
Every runtime entry point asserts isolation first and exits non-zero otherwise.
See docs/ISOLATION-TEST-PLAN.md — that document
is a standing engineering rule, not incident paperwork.
Repository layout
src/
index.ts host half: Config schema, dictionary lifecycle, the route
host/ request handling, the SQLite dictionary, fixture data, route path
shared/ types and text normalization shared by both halves
client/
index.tsx browser runtime: overlay, command, settings, listeners
gesture.ts pure gesture classifier + identity (no DOM, no I/O)
trigger.ts pure trigger gate: switches, eligibility, de-duplication
lookup.ts request identity, supersession, the card's published state
selection.ts selection qualification and live-Range geometry
card.tsx the shell.overlay occupant
fixtures/ generated fixture database (gitignored; see `npm run build:fixture`)
lib/ build output; committed because the loader reads it directly
scripts/ build, verification and isolated-test tooling
docs/ evidence and engineering rules
tests/ unit tests (vitest)
Documentation
docs/RECOVERY_AUDIT.md— what was verified and whendocs/ISOLATION-TEST-PLAN.md— mandatory test isolationdocs/PHASE1_EVIDENCE.md— Phase 1 runtime resultsdocs/PHASE2_EVIDENCE.md— gesture classification resultsdocs/PHASE3_EVIDENCE.md— SQLite dictionary resultsdocs/PHASE4_EVIDENCE.md— trigger gate, de-duplication and request ordering
License
MIT.