The old suite exercised the built lib/ bundle, so v8 remapped almost
nothing (8% src coverage) and every run needed a build first. The new
suite imports src/ directly under two vitest projects (node for
host/shared, jsdom for client), rendering real React 18 trees against
the real dictionaries and ui-primitives, with faithful in-memory
harness faces (cordis inject/effect, locale chain, slots, sessions
provide) instead of plugin mocks.
- 43 fine-grained, stateless, single-module spec files (606 tests)
forked across workers: ~2.5s wall, no build step
- v8 thresholds at 100/100/100/100 per file over src/**
- host specs drive the real projection definitions (init/apply/view)
plus one full cordis + SessionStore + SessionProjectionRegistry
integration; the framework's snapshotJsonValue pins the
projection-cache precondition on every intermediate fold state
- oxlint's src ruleset now covers tests/** as well
- typecheck covers the tests via tsconfig.tests.json
Introduce `pnpm run coverage` to run vitest with v8 instrumentation,
generate a browsable HTML report under ./coverage/, and gate it from
git. Scope coverage to the src tree only: lib/ is the tsdown build
artifact and has no executable surface to cover. Exclusions are
limited to pure type modules (ambient .d.ts, src/shared/types.ts,
src/host/compat.ts) so the summary reflects runnable code.
Register the dsh-context settings namespace on the Host (dsh-settings seam,
schemastery schema, optional composition) and claim its Plugin configuration
card in the browser (settings.plugin.item, keyed by namespace). The card
edits the default trend granularity the Context tab opens with; in-chart
toggling stays mount-local and never writes back.
Degrades cleanly: no settings provider -> no registration, no card, step
default; remote browser (memory mode) -> unavailable scope, no card.
vitest (esbuild) and Node 24 type stripping run the .ts files directly;
helper imports go extensionless like src. Typecheck stays scoped to src/
as before.
Mirror dsh's own lint setup (oxlint 1.76.0 pinned exact, oxlint-tsgolint
7.0.2001 with options.typeAware, correctness category off, hand-picked
rule list; jsPlugins: eslint-plugin-sonarjs + @stylistic/eslint-plugin
with the official style profile: indent 2, semi never, single quotes,
comma-dangle multiline, max-len 140). Scope mirrors official too: src TS
only, generated/bundled output (lib, *.js, *.mjs, *.config.ts) ignored.
Wiring: pnpm run lint / lint:fix scripts, plus a Lint step in the
release workflow between Build and Test.
Fixes applied (runtime behavior unchanged; verified against dsh
0.1.0-rc.7 and 0.1.1-rc.2 wire shapes, see below):
- autofix pass: arrow-parens, comma-dangle, eol-last, indent, quotes
- restrict-plus-operands: 31 string+number concatenations -> template
literals (restrict-template-expressions allows numbers)
- no-unused-vars: drop unused imports/destructured names
- no-unnecessary-type-conversion: drop redundant String() where the
field is already string-typed
- no-unsafe-*: type the fetch-json boundary in latestVersion, narrow
unknown before spread/cast instead of trusting any
- no-unnecessary-condition, three honest categories:
* boundary sanitizers (services.timelineOf/headersOf, RawBlocks,
headers.recordOf) now read through Record<string, unknown> instead
of casting first, so the defensive checks are type-meaningful again
* post-sanitize consumers (contextView/headline/assemble/browser)
drop the fallback-empty-array on collections the sanitizer
guarantees
* array-index-undefined gaps (fold turn attach, headers dedupe) use
.at() so the past-the-end guard is real to the type system
- no-non-null-assertion: stackedBar reserve carries the checked max
- no-floating-promises: void the fire-and-forget version check
- intentional-contract suppressions with rationale: react.ts keeps its
injected require (closure-factory loader contract)
- type-honest hardening kept deliberately: MessageSource.sections
entries stay nullable (foreign plugins author snapshot sources) and
isInjection still rejects a null source — foreign message payloads
must never crash the fold
Cross-version audit (0.1.0-rc.7 vs 0.1.1-rc.2, message parsing focus):
- core/session types.ts: only additive optional interrupted on
assistant/message (unread by us); every event payload we fold
(request/header, request/context, tool/call, tool/result,
user/message, assistant/message, compaction/*) is byte-identical
- llm types.ts / message.ts / content.ts block shapes: identical
- token-meter estimate.ts: identical, and official has NO null-entry
guards on content blocks — our removed guards match official trust,
our retained fallback-empty-string / typeof reads stay a leniency
superset
- deriveEventMessage (core/session surface.ts): identical
- ui-conversation chat-nodes contract: unchanged; our client reads are
minimally-typed unknowns by design
- tool/call callId is a branded string in both versions, so the
narrowed typeof guard drops only an impossible coercion
The host half value-imports zod (Config schema, timeline/headers wire
validators) and the build keeps it external, so a profile install without
zod would fail at plugin load. optional:true would silence the one
warning that catches that. react and dsh-client-ui-primitives stay
optional: only the browser half uses them and the module table provides
them there.
- tsdown.config.ts gains the three CSS virtual loaders mirrored verbatim
from packages/client/tsdown.client.ts: .module.css (lightningcss-hashed
class map + tagged injection), .css?inline (compiled text), and plain
.css (unhashed tagged injection), each registered as a watch dependency
- src/client/styles.ts (223 rules as a string array) becomes
src/client/styles.css — real stylesheet authoring, minified by
lightningcss at build (client bundle 134.8 -> 130.3 kB)
- injection now happens at factory materialization with the official tag
shape (data-plugin + data-plugin-css + dedupe guard), replacing the
hand-rolled ctx.effect; the HMR receiver owns removal by data-plugin,
so the hmr spec pins that lifecycle instead of fiber disposal
- Decision: class names keep the flat lc-* namespace (the anti-collision
discipline, per the research note) instead of per-component hashed
modules — the stylesheet's rules are interconnected across components
(parent:hover child, shared state classes) and 241 test selectors read
literal classes off the built bundle, which a content hash would make
unknowable; the hashed .module.css channel is mirrored and available
for future self-contained components
- globals.d.ts declares the *.css / *.module.css module shapes for the
strict typecheck
- vitest is the runner (matches the official harness); the two bare-node
scripts and the manual repro become 13 spec files under tests/
- the 1637-line client.test.mjs splits into focused modules (chart /
browser / linkage / composition-link / tool-chip-bridge /
browser-autoload / images / modal / resilience), each booting a fresh
view bed from tests/helpers/viewBed.mjs — the stateful fake-React
harness, fake ctx, fixtures, and tree walkers now live in one place
- tests/hmr-safety.client.spec.mjs (jsdom lane, @vitest-environment):
fiber dispose removes every registration — locale dicts, slot rows,
trigger source, provide contribution, and the plugin-owned style tag
- host spec gains the same dispose-removes-registrations pin for the two
session-projection units
- the real-React jsdom mount (was npm run repro, broken since the tsdown
switch by its own string surgery) now runs the bundle verbatim through
__ModuleLoader__ and is part of the suite; fixed two latent harness
bugs along the way: the slot-register capture was overwritten by the
later overlay registration, and the granularity query needed scoping
to the trend row (the events card reuses the pill-button classes)
- bundle evaluation in all specs runs the artifact verbatim via the
boot handoff — no string surgery, decoupled from bundler layout
- scripts.test is now typecheck + vitest run; scripts.repro is gone
- tsdown host config now emits lib/index.d.ts (Config zod schema,
timeline/header/snapshot wire types, plugin entry surface)
- package.json gains top-level "types" and a types condition on the
"." export, mirroring the official exports shape
- the d.ts inlines the type-only dsh-session-projection wire layer as
ambient module declarations, so consumers typecheck without extra
installs; verified with a synthetic consumer (tsc nodenext, full lib
check) — remaining upstream d.ts lib requirements unchanged
- ./client keeps no types condition: its surface is the CJS
module.exports handoff consumed by the browser loader, not TS
importers (the official client bundle config is likewise dts:false)
- banner/intro/footer three-part closure-factory wrap (verbatim from
packages/client/tsdown.client.ts), intro now owns var module/exports
- deps.neverBundle/alwaysBundle guards: host keeps production deps
external, client keeps only module-table specifiers external
- bundle purity gate: any non-requested @deepseek-ai/* value import is
a build error (INLINE_SAFE / vendored-library / generated-remote
allowances mirrored)
- NODE_ENV + import.meta.env defines for inlined node-idiom deps
- client sourcemap on, matching the official artifact contract
- client smoke test now runs the bundle verbatim through the
__ModuleLoader__ handoff instead of slicing the factory body by
exact output strings (decoupled from bundler layout)
zod is already a dependency of @deepseek-ai/dsh-session-projection,
so the host dsh environment always provides it. Moving it to
peerDependencies avoids duplicate zod installations and lets the
plugin resolve the same zod instance as the framework.
- dependencies: remove zod (now empty)
- peerDependencies: add zod ^4.4.3
- peerDependenciesMeta: mark zod as optional
- tsup.config.ts: update comment to reflect peer dep status
- version: bump 0.20.0 -> 0.20.1
Lower @deepseek-ai/dsh-client-ui-primitives, @deepseek-ai/dsh-session and
@deepseek-ai/dsh-session-projection peer and dev deps from ^0.1.0-rc.8 to
^0.1.0-rc.7 so the package declares compatibility with the DSH Community
Market desktop runtime (currently @deepseek-ai/dsh 0.1.0-rc.7).
Replace the hand-rolled esbuild script (scripts/build.mjs) with a single
tsup config that already existed but never ran: tsup re-bundles the config
into a temp .mjs, so import.meta.url pointed at the temp dir and the
package.json read failed, silently leaving no entries. Read the manifest
from process.cwd() instead. The host half now keeps @deepseek-ai/dsh-session
external (resolving to the harness's single runtime instance) instead of
inlining it; the client half keeps the __ModuleLoader__.load wrap.
Align deps with the official dsh convention: runtime harness packages
(cordis, dsh-session, client-ui-primitives, react) move to peerDependencies
with dev mirrors (client-only ones optional); zod stays a dependency;
exact rc pins become caret ranges (^0.1.0-rc.8) so rc9/0.2 releases don't
break installs. Drop the esbuild devDependency (tsup brings its own) and
add tsup itself.