dsh-code-impact
Post-edit impact checks for DeepSeek Harness — a symbol index, a reference graph, and an inheritance model that answer "what did this edit touch?" instead of "grep the repo and re-run everything".
Why
Every small edit — a default value, a rename, a deleted method — triggers the same ceremony: search the whole repo, open a pile of files, run the full type-check and the full test suite. The problem isn't wanting full coverage. The problem is doing it by scanning.
An index inverts that:
- the whole repo's relationships are already in memory, so a "full check" is one query, not a scan;
- after an edit, only the changed file is re-parsed, and the reference edges tell you exactly which sites must be revisited;
- answers never say "probably fine" — they give
file:line, a confidence, and an explicitly listed unverified part.
What you get
Two tools, registered into every agent session — plus a browsable page of the same index (Settings → Code Impact).
code_query
op |
Answers |
|---|---|
find |
where is this symbol (addNode, Graph.addNode, ts:src/x.ts#Graph.addNode) |
outline |
declaration skeleton of one file, with line ranges, exports, heritage clauses |
refs |
who mentions this symbol — each site resolved individually, so a same-named local doesn't count |
hierarchy |
extends / implements / subtypes / members (own, inherited, overridden) |
search |
substring match over names and signatures |
status |
index size, root, cache location, engine mode, parse failures |
find "parseTimeout" — 1 match
ts:src/config.ts#parseTimeout
variable src/config.ts:1-1 exported refs:3
parseTimeout = 30 ;
refs ts:src/config.ts#parseTimeout — 3 sites (exported, refs:3)
variable src/config.ts:1 parseTimeout = 30 ;
confidence: exact 3 · resolved 0 · heuristic 0 · unresolved mentions 0
src/policy.ts:4 value in RetryPolicy
timeout = parseTimeout;
src/service.ts:5 value in makeService
const policy = buildPolicy({ timeout: parseTimeout });
code_verify
Call it after editing instead of re-scanning. It re-parses only the files that changed on disk (and discovers new ones), then reports a symbol-level diff:
verify — verdict: internal
1 file changed (0 deleted) · +0 -0 symbols · 1 body-only
body-only (signature unchanged, callers unaffected):
ts:src/config.ts#parseTimeout refs:6
verify — verdict: breaking
1 file changed (0 deleted) · +1 -1 symbols · 1 contract
symbols that no longer exist:
ts:src/config.ts#parseTimeout
references that no longer resolve:
dangling src/policy.ts:4 parseTimeout
verdict has four levels: clean (nothing changed), internal (implementation only),
contract (signature/export changed, callers may need edits), breaking (some reference no
longer resolves).
A reference is only called dangling when it used to resolve to the symbol that went away. A
mention that never resolved in the first place — filepath.Base, a package that is not installed,
a name the heuristic merely guessed at — is not reported, because a false alarm costs more than a
missed one.
The rule that makes it useful in practice: const timeout = 30 → = 60 is a value change,
reported as body-only, never as a contract change. Containers (class / interface / struct) are not
listed themselves — their members already express the change, so one memo edit doesn't drag the
whole class into the report.
code_verify is incremental: it reports changes since the index last synced. Ask twice without
editing in between and the second answer is clean. That's the intended loop — one question after
each edit, and the answer is about what you just did.
The index page
The same index is browsable: Settings → Code Impact. Three views over the workspace, drawn from the index rather than from a fresh scan:
| view | what it answers |
|---|---|
| Architecture | a treemap of directories weighted by symbol count, coloured by language — where the code actually lives |
| Imports | the file-level import graph as a chord diagram; hover a file to isolate its edges |
| Inheritance | the type hierarchy by depth, implements drawn dashed |
| Sessions | which sessions used the plugin, what they asked, what came back |
Clicking a file lists its symbols with kind, signature and line; clicking a type shows its
supertypes, subtypes and the file that declares it. The header carries the honesty chips —
files / symbols / names / languages, when the index was built, and a warning when files were
skipped — plus a Refresh button that re-checks the disk before redrawing.
The Sessions tab is the plugin's own audit trail: every code_query / code_verify call is
recorded per session (id, working directory, call counts, and the last 40 calls with their target
and one-line outcome). It is written to $DSH_HOME/cache/code-index/usage.json, so it survives a
restart and covers sessions this process never saw. It records the plugin's own calls only — it is
not an authority on what the agent did — and Clear empties it without touching the index.
Open full page puts the same map in a browser tab (/code-impact/app) with more room and no
settings chrome behind it; the two views share the same routes and the same index.
The page draws one indexed workspace at a time, and the folder box under the header is how a
second one gets in: type an absolute path (D:\work\my-project), press Enter, and the page builds
an index for it and switches to it. The same workspace stays in the dropdown afterwards. This is
the only way a workspace appears without the agent using a tool in it first — a session whose cwd
is the project also indexes it, implicitly, the first time code_query runs.
It is a client half of the bundle (client.js, React, theme tokens only — no iframe, no inline
HTML), talking to the host through read routes, one build route, and the usage routes. The
standalone page (web/app.html + web/app.js, plain DOM and SVG) is served from the same host:
GET /code-impact/api/roots
GET /code-impact/api/overview?root=<abs path>&refresh=1
GET /code-impact/api/file?root=<abs path>&path=<repo-relative path>
POST /code-impact/api/index {"root": "<absolute path>"}
GET /code-impact/api/usage?limit=100
POST /code-impact/api/usage/clear {}
GET /code-impact/app the full-page view
GET /code-impact/app.js
Reads are allow-listed: the page can only read a directory that is already indexed, or the
session's working directory. POST /index is the one route that does work on request — it builds a
cache for the exact directory you named and nothing else, refuses relative paths, missing
directories and plain files with a 400 carrying the reason, and never writes inside your
repository.
Install
dsh-code-impact is a DSH host bundle: zero dependencies, plain ESM, one patch row.
# from GitHub (or any published tarball / registry package)
dsh plugin --profile desktop add github:Locael123/dsh-code-impact
dsh web # restart: the host half of a plugin is imported once, at boot
# from a local checkout
plugin_manager install_bundle <absolute path>/dsh-code-impact
Manual install — only if the package is already resolvable from the profile's node_modules
(plugin_manager does that for you; a bare cordis.yml row alone will not):
- id: dsh-code-impact
name: dsh-code-impact
config:
maxFiles: 20000
The bundle ships its own cordis.patch.yml, so the plugin manager path is preferred — it wires the
patch row, links the directory, and keeps files[] correct.
| config | default | meaning |
|---|---|---|
maxFiles |
20000 |
cap on files per build; truncation is reported in status |
root |
session working directory | override the indexed root |
Cache: $DSH_HOME/cache/code-index/<hash(root)>/ — content-addressed facts/ plus a
manifest.json. Editing one file rewrites one fact. It is a cache: delete it any time, it
rebuilds; it is never written inside the indexed repo.
Precision contract
Every answer carries its own bounds instead of a confident guess:
confidence: exact | resolved | heuristic | ambiguous—exactresolves to a concrete declaration;heuristicis unique-name / nearest-path inference;total/truncated— truncation is stated, not hidden;unverified:— what this particular answer does not cover (cross-language edges, dynamic references, type-level compatibility).
A file that fails to parse never disappears silently: code_query op=status reports
warning: N file(s) could not be parsed and are NOT indexed, and code_verify states it in
unverified:. If the index is under-counting, you get told.
When in doubt it returns candidates rather than "not found".
Scope & limits
- Languages: TypeScript / JavaScript / Go (
.ts .tsx .mts .cts .js .jsx .mjs .cjs .go). - Module resolution: relative paths with extension and
/indexprobing,tsconfig/jsconfigbaseUrl+paths,go.modmodule prefix with same-package directories. - Skipped:
node_modules.gitdistbuildoutvendortarget__pycache__.venvand friends, files over 512 KiB, binaries. - Deliberately approximate: TS
implementsand Go interface satisfaction are structural / implicit, so they yield a candidate set markedheuristic. This is the only place the tool knowingly guesses. - Not modelled: cross-language edges (a Go interface to its implementors, a JSON schema to its consumers), dynamic dispatch, generated code.
How it works
- Facts, not graphs. Each file stores only what this file says:
imports,exports,decls(with heritage and members),refs(name + position + role + lexical scope). There is no target ID on disk. Name → definition is resolved at query time, which is why editing one file invalidates exactly one file. - Stable SymbolId:
<lang>:<path>#<qualname>, no line numbers. A rename or move changes the ID, and that change is itself the signal. - Freshness: a query
stats the files in its answer before returning (mtime+size), so a stale hit is impossible for anything it reports.code_verifydrives incremental re-indexing; external edits are caught by the same stat check; a structural change bumps a manifest version and rebuilds. - No native modules, no LSP, no subprocess. A hand-written lexer plus a scope-aware resolver per language keeps cold start in the hundreds of milliseconds and the whole plugin dependency-free.
Measured on a 42-file mixed TS/Go workspace: cold build 276 ms, 883 symbols; queries 1–4 ms; warm start from manifest 3 ms for a 5-file fixture.
Development
node test/selftest.mjs # index + tools, 36 assertions
node test/addroot.mjs # the page's "index a folder" path, including its refusals
node test/usage.mjs # the usage ledger: per-session records, persistence, clearing
node test/webcheck.mjs <repo> # the JSON routes the page calls
node test/clientsmoke.mjs # client half registers its settings section
The self-test copies test/fixture — a deliberately small TS + Go project with a tsconfig path
alias, a class hierarchy, and a two-package Go module — into a temp directory, builds a real index,
and runs 36 assertions: body-only edits are not reported as contract changes, a renamed export
surfaces its dangling references, a renamed directory does not, new files are picked up, deleted
files are reported, Go cross-package references resolve exact, warm start reloads from the
manifest, and a file that mentions constructor still parses (that one was a real bug: a plain
object used as a delimiter stack hit Object.prototype.constructor).
No comments yet. Be the first to write one.