# dsh-insights-kit — architecture notes A standard Cordis **bundle** plugin for dsh (DeepSeek Harness), built to the same engineering conventions as `dsh-file-explorer-kit` / `dsh-workspace-kit`: one Loader entry, two faces, one build. ## Two faces, one package ``` src/host/index.ts ──build──▶ lib/index.js (ESM, node; runtime deps: node builtins only) src/client/index.ts ──build──▶ lib/client.js (CJS body in the window.__ModuleLoader__.load envelope) src/cli.ts ──build──▶ lib/cli.js (ESM, node + shebang; the `dsh-insights-kit` bin) ``` - `package.json` `main`/`exports["."]` → host face; `exports["./client"]` + `dsh.client` metadata → the browser face served over `/plugins`. - `cordis.patch.yml` carries a single `insert` entry (`id: insights`, `name: dsh-insights-kit`) — the dsh client module system rejects multiple active sources resolving to one package name, so one entry carries both faces. - The client bundle externalizes only `react` / `react/jsx-runtime` (satisfied by the shell-seeded platform baseline); `@deepseek-ai/*` imports are type-only everywhere, so the host artifact needs nothing beyond the loader's own runtime. ## Data flow ``` dsh-insights.com/data/{insights,scenarios,dynamics,compat,enrich}.json (open dataset, regenerated ~daily) (DSH_INSIGHTS_UPSTREAM_BASE overrides all docs to one origin) ▲ lazy fetch on first request, in-memory cache, TTL 6h │ (failed fetches never poison the cache; concurrent firsts share one in-flight promise) host face: GET /dsh-insights/* on ctx.webServer (read-only, JSON) ▲ same-origin fetch client face: ✦ button in the official sidebar.footer.action slot opens the 「生态」drawer registered in the shell.overlay slot ``` The browser never talks to dsh-insights.com directly — everything rides the host routes, so the loopback trust posture of the dsh web server is the only network boundary. (Installed-plugin enumeration is the one exception that isn't data traffic: a local Typert Remote call, see below.) ## Host routes (`src/host/index.ts`, prefix `/dsh-insights`) | Endpoint (GET) | Description | |---|---| | `/plugin?full_name=owner/repo` | One plugin's health card, trimmed: `full_name/stars/grade/score/dimScores/drops/npm/version/description/url`. Upstream drops are bare code strings; the host enriches them to `{code, sev, label:{zh,en}}` via the health-v5 rule table (`src/host/drops.ts`). The response also carries `similar`: top-5 same-category picks from `enrich.json` (score desc, stars tiebreak, self excluded; empty when no category; a failed enrich fetch degrades to `[]`). 404 `not-in-corpus` when absent. | | `/search?q=&limit=20` | Case-insensitive substring match over `full_name` + `description`, ranked by stars desc; compact rows without `dimScores`/`drops`. Limit capped at 50. | | `/audit?npm=a,b,c` | Batch health lookup keyed by npm package name (the 体检 Audit page): each name maps to a trimmed card (matched on the corpus row's `pkgName`, case-insensitive) or null when unlisted, with a `compat` slice attached to hits (`enginesDsh` + the first 3 `dshPeers`, joined from `compat.json` on npm name; a failed compat fetch degrades to no annotation). Comma-separated, capped at 100 names. | | `/scenarios` | `scenarios.json`, with each pick annotated by its npm `pkgName` (joined from the corpus on `full_name`, omitted when unknown) so the client can offer copyable install/uninstall commands. | | `/dynamics` | `dynamics.json` passthrough, with the dsh npm dist-tags overlaid by a live registry fetch (5-min TTL, best-effort: failure keeps the snapshot's tags). | | `/runtime` | The running dsh version, resolved host-side via `createRequire(import.meta.url)` from `@deepseek-ai/dsh-web-app/package.json` (fallback `@deepseek-ai/dsh-base/package.json`); `null` when neither resolves. Local-only — no upstream fetch. | | `/health` | Liveness + per-document cache age/staleness. | Every request passes a host-trust gate mirroring the official `/api` fence (loopback Host trusted outright; otherwise a same-origin Origin marker is required). This is not an auth layer — same posture as the official web server, which binds 127.0.0.1 by default. Upstream errors surface as `502 { ok:false, error:{code:'upstream'} }`; malformed input as 400; missing corpus entries as 404. All responses carry an `ok` envelope (`{ ok:true, ... }` / `{ ok:false, error:{code,message} }`). ## Author self-check (`src/host/selfcheck.ts` + `src/cli.ts`) The author self-check is a **CLI**, not a panel section: `dsh-insights-kit selfcheck [--json] [--lang zh|en]` (the package's `bin` entry → `lib/cli.js`) wraps `runSelfcheck()` — the [dsh-plugin-health](https://github.com/ice5kysl/dsh-plugin-health) CLI's `--dir` capability as a plain library function: - **Input validation**: the path must be absolute, contain no `..` segments, and resolve to an existing directory with a parseable `package.json` (`invalid-path` / `no-package-json` / `bad-package-json` / `not-a-directory` — CLI exit code 2). Everything it reads stays under that directory. - **Scoring**: the health-v5 rulebook applied to the on-disk layout — manifest (`dsh.bundle.patch` declared + patch file present, `main = lib/index.js`, `exports["./client"]`, `files` whitelist, built `lib/` artifacts), docs (README presence + 400-byte floor, zh README, description, `docs/`), repo (LICENSE, keywords as the local topics proxy), engineering (tests, CI), and npm consistency (unpublished / version drift / single release / >90-day-stale, from the registry with a 10s timeout; registry base overridable via `DSH_INSIGHTS_NPM_REGISTRY`, and an unreachable registry skips npm rules instead of guessing). Weights and grade thresholds mirror the site: fail −20 / major −10 / warn −5 / minor −2 from 100, S≥95/A≥90/B≥75/C≥60/D. Every deduction carries per-code fix guidance (`fix: {zh, en}`). Rules needing GitHub/git metadata (topics, activity, single-push, batch-import) are returned in `uncovered` with reasons and never scored. - **Read-only surface scan**: ported from the CLI — fs writes (incl. bare `writeFileSync(` imports, which the CLI missed), child processes (`.exec(` regex calls excluded via lookbehind), HTTP write verbs (`Map.delete` etc. excluded the same way), sanitization references and `dangerouslySetInnerHTML`. Informational only — never affects the score. - **CLI output**: text mode prints score + grade, deductions grouped by category with fix text (language from `--lang`, default `$LANG` zh* → 中文), zero-weight advisory **hints** (`hints: [{code, zh, en}]` — currently `manifest.no-engines-dsh` when no `engines.dsh` range is declared; never scored, never affecting the exit code), the scan summary, and the uncovered-rule codes; `--json` prints the full report. Exit code is **1 when any fail-tier deduction exists**, else 0 — usable as a CI pre-publish gate. ## Caching (`src/host/upstream.ts`) - One cache entry per upstream document (`insights` ≈ 6 MB, `scenarios`, `dynamics`), keyed by name, storing `{ data, fetchedAt }`. - TTL 6 hours: the site regenerates roughly daily, so 6h keeps data fresh without hammering a static host; staleness is reported by `/health`. - Lazy: nothing is fetched until the first request that needs the document. - Failure-safe: a rejected fetch deletes only the in-flight marker, so the next request retries; a previously cached (now stale) document is *not* served in place of an error — callers get a clean 502 instead of silently-old data. - Injectable (`baseUrl` / `fetchJson` / `ttlMs` / `now`) so `tests/smoke.test.mjs` runs the whole surface against a local fixture server with zero network access; `DSH_INSIGHTS_UPSTREAM_BASE` overrides the origin for the same purpose at runtime. ## Client face (`src/client`) Entry-point seams (both official, additive — the same ones `dsh-workspace-kit` uses, whose usage we verified in its source): - `sidebar.footer.action` (list/root): an always-visible ✦ button (`SidebarAction.tsx`, `id: 'insights-kit.action'`). Slot components share no ctx, so the click reaches the panel via a `dsh-insights-kit:toggle-panel` window event — the same idiom as the workspace-kit sidebar toggle. - `shell.overlay` (list/root): the panel (`InsightsPanel` in `InsightsView.tsx`, `id: 'insights-kit.panel'`). The slot is additive and click-through until an entry opts into pointer events, so the component stays mounted, renders `null` while closed, and draws a fixed backdrop + right-side drawer while open (Escape / backdrop click closes). This is why the entry moved off `conversation.view`: the toolkit is session-independent, and the drawer is reachable from every screen, not only inside a session. Three capability sections, each fetching lazily on first visit (tab order: 体检 / 场景 / 查验): 1. **体检 Audit** — installed-plugin health check (see the enumeration note below): a「当前 dsh 版本 · 最新 release」line (running version from `/runtime`, latest from the `/dynamics` dist-tags — overlaid live from the npm registry so it tracks the day's release, with an upgrade hint when behind on base versions), per-plugin grade badge + score, an S/A/B/C/D summary bar, npm version-drift badges (`npmLatest ≠ version`), a per-row dsh-compat line (`engines.dsh`, else the cordis peer range; ✓/⚠ verdict when the running version is known and the range is a simple ^/~ range — conservative base-version check, prerelease tags dropped, see `src/shared/compat.ts`), "better alternatives ↗" links on C/D rows (to the plugin's dsh-insights.com page, which carries same-category recommendations), a BREAKING-release alert card fed by `dynamics.json`, and a copyable uninstall command per row. Unlisted plugins render as 「未收录」rows. 2. **场景 Scenarios** — scenario cards with recommended plugin rows; each row carries an「已安装」marker when the plugin is already installed (same inventory → audit chain as Audit) and a copyable install/uninstall command built from the host-annotated `pkgName`. Clicking a row jumps to Check with that plugin loaded. 3. **查验 Check** — input routed by `classifyCheckInput`: `owner/repo` or a pasted GitHub URL (`parseRepoInput`) → `/plugin` → health card (grade badge S 紫/A 绿/B 蓝/C 橙/D 红, score, dimension bars, severity-colored deduction list, npm-latest drift hint, link out to `https://dsh-insights.com/p///`, an install/uninstall action row — copyable `dsh plugin add/remove` command with an「已安装」marker when the plugin is in the inventory, a source-install note + GitHub link when unpublished — and a「相似推荐」section fed by the response's `similar` list, each pick loading its own card on click). A bare keyword (no `/`) → `/search`, a debounced (300 ms) corpus search list (grade badge + stars + truncated description per row, stale in-flight responses discarded via a generation counter), each hit loading its card on click. A 「不在权威集」miss auto-searches the repo name once and lists similar plugins under the notice. Rows in Audit/Scenarios jump here. The panel stays strictly read-only: install/uninstall is never performed in-app — the buttons only copy the `dsh plugin --profile web add/remove` command to the clipboard (run it in a terminal, then restart `dsh web`). ### Installed-plugin enumeration (research conclusion) The seam exists and is official: `@deepseek-ai/dsh-host-plugin-inventory` ships a read-only Typert Remote (`pluginInventory/list`) projecting the live Cordis Loader entries `{ moduleName, enabled, fiberPhase }`, and the client-side gateway (`ctx.remote`, provided by dsh-api-gateway — a core service, so injecting `remote` is safe) exposes `TypertClientRemote.$mount(contribution)` to mount any generated Remote contribution at runtime from a dynamic plugin. We mount an inlined copy of the generated descriptor with a `src-json` result codec instead of importing the official artifact, because the artifact's only import is `zod`, which is not guaranteed to resolve through the dsh client module system for a dynamic plugin; the wire shape is identical, only client-side validation strictness differs (`src/client/inventory.ts`). Loader `moduleName`s reduce to npm name guesses (`src/shared/installed.ts`): scoped/plain names pass through (subpaths trimmed), path-like specifiers (local `dsh plugin add ` installs, including `link:`-prefixed ones) reduce to their basename; the official `@deepseek-ai/*` baseline and disabled entries are excluded, as is anything that is not npm-name-shaped after the reduction (Loader-internal pseudo entries like `cordis:include` never reach the audit batch). The resulting names are health-checked in one `/dsh-insights/audit` batch. Graceful degradation is first-class: if the running dsh build does not serve the inventory gateway (mount throws, namespace absent, or `list()` errors), the Audit section renders the **version & compatibility reminder** form — npm dist-tags, recent releases with BREAKING flagged, and advised actions — instead of failing. Verified locally against `0.1.1-rc.2` type artifacts; the live mount can only be confirmed inside a running dsh web (see the repo README's verification note). - Bilingual zh/en: `src/shared/i18n.ts` is pure (importable from both faces); `src/client/locale.ts` resolves persisted preference → `navigator.language` → English, and the header 中/EN toggle re-renders in place. - Styling is inline `CSSProperties` on the shell's CSS variables (`--fg`/`--bg`/`--border`), no UI dependency beyond React. ## Testing / CI `tests/smoke.test.mjs` (run as `npm test`, also in CI) boots a fixture upstream, a fake npm registry, four fixture plugin directories, and a fake `ctx.webServer`; it covers: trimming + drop enrichment (+ `npmLatest`), corpus 404, input validation, search matching/ranking/limits, the audit batch lookup (hit/null/case/400) with its compat slice (engines.dsh + ≤3 peers), the runtime-version probe shape, the inventory-entry → npm-name mapping and filtering incl. pseudo-entry/link: handling (imported from `src/shared/installed.ts`, node type-stripping), the conservative ^/~ range check (`src/shared/compat.ts`), the scenarios `pkgName` annotation, the 查验 input router (`classifyCheckInput` from `src/client/api.ts`), the plugin route's `similar` annotation (same-category top 5, self excluded, empty without a category) and pkgName passthrough, self-check as a library (`runSelfcheck()`: well-built S/100, skeletal plugin's full deduction set, write-surface scan kinds, npm drift/single/stale, path validation errors) and as a CLI subprocess (`lib/cli.js` exit codes 0/1/2, `--json`, `--help`), both passthroughs, cache health, the trust gate (403/200), upstream-failure → 502, and cache isolation across instances. CI: `npm install → typecheck → build → smoke → npm pack --dry-run`.