refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:54:38 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/README.md
README.md: 3b472492ba12146101bae0e2eae9c837a7f6cb1d
README.zh.md: 09ca13e4bc22cac746bd60e1898519a43320ae35
README.md: 414311fd3f537634651daf6f313baf33daede26b
README.zh.md: 8791b0ea9c01ed637586cee5c131690a73263a6a
+1 -1
View File
@@ -7,7 +7,7 @@ The language-server capability seam: an LSP Service Definition, a generic stdio
| Package | Role | ctx key |
|---|---|---|
| `lsp/` | Service Definition (provider registry by branded id + extension mapping, per-query selection, vocabulary, `LspError`) | `ctx.lsp` |
| `lsp-local/` | Generic multi-server stdio backend over `ctx.fs` and `ctx.subprocess` (JSON-RPC, transient-open queries) | (registers providers on `ctx.lsp`) |
| `lsp-stdio/` | Generic multi-server stdio backend over `ctx.fs` and `ctx.subprocess` (JSON-RPC, transient-open queries) | (registers providers on `ctx.lsp`) |
| `tool-lsp/` | Model-facing `lsp` tool (four operations, one-based UTF-16 cursor coordinates) | (registers on `ctx.tools`) |
The Service Definition lives at `lsp/lsp/`. The seam exposes exactly four semantic operations — `goToDefinition`, `findReferences`, `goToImplementation`, `hover` — and no generic JSON-RPC escape hatch, so a provider swap does not change how the model asks for navigation and no protocol payload or unreviewed mutation reaches the model contract. Providers register **capabilities**, not tools; `tool-lsp` is the only owner of the model-facing name, schema, prompt guidance, and presentation.
+1 -1
View File
@@ -7,7 +7,7 @@
| 包 | 职责 | ctx key |
|---|---|---|
| `lsp/` | Service Definition(按品牌化 id + 扩展名映射组织的提供方注册表、逐查询选择、词汇、`LspError` | `ctx.lsp` |
| `lsp-local/` | 基于 `ctx.fs``ctx.subprocess` 的通用多服务器 stdio 后端(JSON-RPC、查询时临时打开文档) | (在 `ctx.lsp` 上注册提供方) |
| `lsp-stdio/` | 基于 `ctx.fs``ctx.subprocess` 的通用多服务器 stdio 后端(JSON-RPC、查询时临时打开文档) | (在 `ctx.lsp` 上注册提供方) |
| `tool-lsp/` | 面向模型的 `lsp` 工具(四种操作、从 1 开始的 UTF-16 光标坐标) | (注册到 `ctx.tools` |
Service Definition 位于 `lsp/lsp/`。该 seam 恰好公开四种语义操作:`goToDefinition``findReferences``goToImplementation``hover`,且不提供通用 JSON-RPC 逃生口;因此,替换提供方不会改变模型请求导航的方式,也不会让协议载荷或未经评审的修改进入模型约定。提供方注册的是**能力**而非工具;`tool-lsp` 是面向模型的名称、schema、提示词指引和呈现的唯一 owner。
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/lsp-local/README.md
README.md: 661c27d3326adfc3408b33550a63fe3ebe183a39
README.zh.md: 9bc66ee00ff2f21df66b88aa607a555fd56e974b
# pnpm run verify-translation-pairing --write packages/lsp/lsp-stdio/README.md
README.md: beadc34ec738ae3f6511cae764240c0e9d8c61d7
README.zh.md: 2d6516a25147fb7593e8d45d843315098cdff7d3
@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-lsp-local
# @deepseek-ai/dsh-lsp-stdio
English | [中文](README.zh.md)
@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-lsp-local
# @deepseek-ai/dsh-lsp-stdio
[English](README.md) | 中文
@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-lsp-local",
"name": "@deepseek-ai/dsh-lsp-stdio",
"description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/lsp/lsp-local"
"directory": "packages/lsp/lsp-stdio"
},
"type": "module",
"main": "lib/index.js",
@@ -1,6 +1,6 @@
/**
* Shared cancellation helpers for the local LSP provider's host-I/O, queue, and protocol phases.
* @module @deepseek-ai/dsh-lsp-local/abort
* @module @deepseek-ai/dsh-lsp-stdio/abort
*/
import { timeoutOf } from '@deepseek-ai/dsh-timeout'
@@ -7,7 +7,7 @@
* fatal close, and exposes tree-scoped termination through the handle so the
* instance owns teardown; group/tree mechanics live in the subprocess
* Service provider.
* @module @deepseek-ai/dsh-lsp-local/connection
* @module @deepseek-ai/dsh-lsp-stdio/connection
*/
import type { Writable } from 'node:stream'
@@ -104,7 +104,7 @@ export class LspConnection {
})
/* v8 ignore start -- 'pipe' dispositions expose both streams by the seam contract; defensive. */
if (this.handle.stdin === undefined || this.handle.stdout === undefined) {
throw new Error('lsp-local: subprocess implementation dropped a piped protocol stream')
throw new Error('lsp-stdio: subprocess implementation dropped a piped protocol stream')
}
/* v8 ignore stop */
this.stdin = this.handle.stdin
@@ -2,7 +2,7 @@
* LSP base-protocol framing: `Content-Length`-delimited JSON-RPC over a byte stream. The encoder
* produces one framed buffer; the decoder buffers incoming bytes and yields complete message bodies,
* bounding the header and total message size so a hostile or broken server cannot exhaust memory.
* @module @deepseek-ai/dsh-lsp-local/framing
* @module @deepseek-ai/dsh-lsp-stdio/framing
*/
/** The header/body separator in the LSP base protocol. */
@@ -8,7 +8,7 @@
*
* Namespace plugin (named exports, no default export). Lifecycle is effect-scoped: disposal
* unregisters from `ctx.lsp` and tears down every live server.
* @module @deepseek-ai/dsh-lsp-local
* @module @deepseek-ai/dsh-lsp-stdio
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -41,7 +41,7 @@ export { LspInstance } from './instance.ts'
export { LspConnection } from './connection.ts'
/** Cordis plugin name for loader diagnostics. */
export const name = 'lsp-local'
export const name = 'lsp-stdio'
/** Services required by this plugin. */
export const inject = ['fs', 'lsp', 'subprocess']
@@ -125,14 +125,14 @@ function throwTeardownFailures(results: readonly PromiseSettledResult<void>[], m
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
const entries = Object.entries(config.servers)
if (entries.length === 0) throw new Error('lsp-local: servers must contain at least one server')
if (entries.length === 0) throw new Error('lsp-stdio: servers must contain at least one server')
const setupAbort = new AbortController()
const stopSetupCancellation = ctx.on('internal/plugin', (fiber) => {
// An async plugin callback must observe its own disposal before Cordis can
// run effect cleanup, because unload otherwise waits for this callback.
if (fiber === ctx.fiber && fiber.uid === null) {
setupAbort.abort(new Error('lsp-local setup disposed'))
setupAbort.abort(new Error('lsp-stdio setup disposed'))
}
})
@@ -140,7 +140,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
// publish an earlier provider. Registry-level mapping conflicts are rolled back below.
const providers = await (async () => {
const lookups = entries.map(async ([providerId, rawConfig]) => {
if (providerId.trim() === '') throw new Error('lsp-local: server ids must be non-empty strings')
if (providerId.trim() === '') throw new Error('lsp-stdio: server ids must be non-empty strings')
const resolved = rawConfig as ResolvedServerConfig
validateServerConfig(providerId, resolved)
const executable = await ctx.subprocess.resolveExecutable(
@@ -180,9 +180,9 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
// Remove every route before process teardown so no new query can enter a draining provider.
for (const dispose of disposers.reverse()) dispose()
const results = await Promise.allSettled(providers.map(provider => provider.disposeAll()))
throwTeardownFailures(results, 'lsp-local provider teardown failed')
throwTeardownFailures(results, 'lsp-stdio provider teardown failed')
}
}, 'lsp-local.registerProviders')
}, 'lsp-stdio.registerProviders')
}
/** Validate one resolved server entry before any provider in the table is registered. */
@@ -202,14 +202,14 @@ function validateServerConfig(providerId: string, resolved: ResolvedServerConfig
/** Reject a timer value Node would clamp instead of scheduling as configured. */
function assertTimer(providerId: string, name: string, value: number): void {
if (!Number.isInteger(value) || value < 1 || value > MAX_TIMER_DELAY_MS) {
throw new Error(`lsp-local: servers.${providerId}.${name} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`)
throw new Error(`lsp-stdio: servers.${providerId}.${name} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`)
}
}
/** Reject a nonpositive or non-integer config value at load, so misconfiguration fails loud. */
function assertPositiveInteger(providerId: string, name: string, value: number): void {
if (!Number.isInteger(value) || value < 1) {
throw new Error(`lsp-local: servers.${providerId}.${name} must be a positive integer`)
throw new Error(`lsp-stdio: servers.${providerId}.${name} must be a positive integer`)
}
}
@@ -246,7 +246,7 @@ class LocalLspProvider implements LspProvider {
private assertActive(signal?: AbortSignal): void {
/* v8 ignore next -- the seam unregisters this provider before disposal; direct in-flight calls
exercise the post-await check instead. */
if (this.isDisposed()) throw new LspError('lsp-local provider is disposed', 'LSP_DISPOSED')
if (this.isDisposed()) throw new LspError('lsp-stdio provider is disposed', 'LSP_DISPOSED')
if (signal?.aborted) throw abortError(signal)
}
@@ -352,7 +352,7 @@ class LocalLspProvider implements LspProvider {
/** Dispose every live instance and block further queries. */
async disposeAll(): Promise<void> {
this.disposed = true
this.lifetime.abort(new LspError('lsp-local provider is disposed', 'LSP_DISPOSED'))
this.lifetime.abort(new LspError('lsp-stdio provider is disposed', 'LSP_DISPOSED'))
const live = [...this.instances.values()]
const draining = [...this.queues.values()]
const resolving = [...this.workspaceLookups]
@@ -364,6 +364,6 @@ class LocalLspProvider implements LspProvider {
])
this.queues.clear()
this.workspaceLookups.clear()
throwTeardownFailures(results, 'lsp-local instance teardown failed')
throwTeardownFailures(results, 'lsp-stdio instance teardown failed')
}
}
@@ -4,7 +4,7 @@
* instance owns one `(provider id, canonical workspace)` process. Queries serialize through a single
* queue so a cancellation that fails to stop the server can terminate it without killing unrelated
* work; distinct instances run in parallel.
* @module @deepseek-ai/dsh-lsp-local/instance
* @module @deepseek-ai/dsh-lsp-stdio/instance
*/
import { LspError } from '@deepseek-ai/dsh-lsp'
@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-lsp-local`.
* @module @deepseek-ai/dsh-lsp-local/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-lsp-stdio`.
* @module @deepseek-ai/dsh-lsp-stdio/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-lsp-local'
const PACKAGE_NAME = '@deepseek-ai/dsh-lsp-stdio'
/** Cordis companion plugin name. */
export const name = 'lsp-local-invariant'
export const name = 'lsp-stdio-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
@@ -3,7 +3,7 @@
* request results (`Location`, `LocationLink`, `Hover`), and the `textDocumentSync` shapes used to
* decide transient-open support. Types only. Fields absent from a real server payload stay optional;
* the translation layer normalizes them into the seam's closed unions.
* @module @deepseek-ai/dsh-lsp-local/protocol
* @module @deepseek-ai/dsh-lsp-stdio/protocol
*/
/** A zero-based UTF-16 position on the wire (the protocol's `Position`). */
@@ -2,7 +2,7 @@
* Pure protocol translation for the local host: what the server's capabilities allow, and how its
* `Location`/`LocationLink`/`Hover` payloads normalize into the seam's closed result unions. No I/O
* or process state every function here is a pure transform, which the fake-stdio tests pin exactly.
* @module @deepseek-ai/dsh-lsp-local/translate
* @module @deepseek-ai/dsh-lsp-stdio/translate
*/
import type {
@@ -8,7 +8,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
/**
* Keyless built-artifact smoke: plain Node imports `@deepseek-ai/dsh-lsp` and
* `@deepseek-ai/dsh-lsp-local` by name through their exports maps, spawns the fixture server, runs
* `@deepseek-ai/dsh-lsp-stdio` by name through their exports maps, spawns the fixture server, runs
* one query (exercising real `Content-Length` framing over `lib/index.js`), and disposes (exercising
* subprocess cleanup). Unit tests use `src/`; this pins the downstream `lib/` path. Skips when `lib/`
* is absent; CI runs it after the build.
@@ -42,12 +42,12 @@ describe.skipIf(!built)('built lib real load path (plain node)', () => {
const script = `
const { Context } = await import('@deepseek-ai/cordis')
const { default: Lsp } = await import('@deepseek-ai/dsh-lsp')
const LspLocal = await import('@deepseek-ai/dsh-lsp-local')
const LspLocal = await import('@deepseek-ai/dsh-lsp-stdio')
const { default: LocalFileSystem } = await import('@deepseek-ai/dsh-fs-local')
const { default: LocalSubprocessService } = await import('@deepseek-ai/dsh-subprocess-local')
const { default: LocalSubprocessRuntime } = await import('@deepseek-ai/dsh-subprocess-local')
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await ctx.plugin(LspLocal, {
servers: {
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it } from 'vitest'
import { fileURLToPath } from 'node:url'
import { LspConnection } from '@deepseek-ai/dsh-lsp-local'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-local/src/connection.ts'
import { LspConnection } from '@deepseek-ai/dsh-lsp-stdio'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-stdio/src/connection.ts'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import { spawnSubprocess } from '@deepseek-ai/dsh-subprocess-local/src/spawn.ts'
@@ -1,5 +1,5 @@
/**
* A scriptable fake LSP server over stdio for lsp-local tests. It speaks the real
* A scriptable fake LSP server over stdio for lsp-stdio tests. It speaks the real
* `Content-Length`-framed base protocol so it exercises the client's framing, initialize handshake,
* transient open/close, request mapping, and teardown without a real language server.
*
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { encodeMessage, MessageDecoder } from '@deepseek-ai/dsh-lsp-local'
import { encodeMessage, MessageDecoder } from '@deepseek-ai/dsh-lsp-stdio'
/** Frame a message the way a server would, for decoder round-trips. */
function frame(body: string): Buffer {
@@ -9,7 +9,7 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { deadline } from '@deepseek-ai/dsh-timeout'
import { canonicalizeWorkspace, readHostSource } from '@deepseek-ai/dsh-lsp-local'
import { canonicalizeWorkspace, readHostSource } from '@deepseek-ai/dsh-lsp-stdio'
const execFileAsync = promisify(execFile)
@@ -6,10 +6,10 @@ import { join } from 'node:path'
import { pathToFileURL, fileURLToPath } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { LspInstance, readHostSource } from '@deepseek-ai/dsh-lsp-local'
import { encodeMessage } from '@deepseek-ai/dsh-lsp-local'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-local/src/connection.ts'
import type { InstanceSpec } from '@deepseek-ai/dsh-lsp-local/src/instance.ts'
import { LspInstance, readHostSource } from '@deepseek-ai/dsh-lsp-stdio'
import { encodeMessage } from '@deepseek-ai/dsh-lsp-stdio'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-stdio/src/connection.ts'
import type { InstanceSpec } from '@deepseek-ai/dsh-lsp-stdio/src/instance.ts'
import type { LspProviderQuery, LspQueryResult } from '@deepseek-ai/dsh-lsp'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import { spawnSubprocess } from '@deepseek-ai/dsh-subprocess-local/src/spawn.ts'
@@ -7,10 +7,10 @@ import { pathToFileURL, fileURLToPath } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import Lsp, { type LspProvider, type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp'
import { deadline } from '@deepseek-ai/dsh-timeout'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
import type { LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-local'
import * as LspLocal from '@deepseek-ai/dsh-lsp-stdio'
import type { LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-stdio'
const fixtureServer = fileURLToPath(new URL('./fixture-server.ts', import.meta.url))
@@ -39,7 +39,7 @@ function fakeServer(fakeEnv: Record<string, string> = {}, overrides: Partial<Lsp
}
}
/** Mount the real seam + lsp-local plugin driving one fake server. */
/** Mount the real seam + lsp-stdio plugin driving one fake server. */
async function mount(
fakeEnv: Record<string, string> = {},
overrides: Partial<LspLocalServerConfig> = {},
@@ -47,7 +47,7 @@ async function mount(
): Promise<Context> {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
const register = ctx.lsp.registerProvider.bind(ctx.lsp)
const registrationSpy = captureProvider === undefined
@@ -75,12 +75,12 @@ function locationJson(line: number): unknown {
return { uri: pathToFileURL(join(ws, 'a.ts')).href, range: { start: { line, character: 0 }, end: { line, character: 3 } } }
}
describe('lsp-local end to end over a fake server', () => {
describe('lsp-stdio end to end over a fake server', () => {
it('routes different extensions to independent configured servers', async () => {
await writeFile(join(ws, 'a.py'), 'x = 1\n')
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await ctx.plugin(LspLocal, {
servers: {
@@ -275,7 +275,7 @@ describe('lsp-local end to end over a fake server', () => {
(registered) => { provider = registered },
)
expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' })
if (provider === undefined) throw new Error('expected lsp-local to register a provider')
if (provider === undefined) throw new Error('expected lsp-stdio to register a provider')
// This implementation-local test reaches the private pool only to synchronize with its actual
// close state. A fixed wall-clock sleep can expire before a CPU-starved child runs its exit timer.
const instances = (provider as unknown as {
@@ -354,7 +354,7 @@ describe('lsp-local end to end over a fake server', () => {
it('waits for every owned teardown before aggregating instance failures', async () => {
let provider: LspProvider | undefined
const ctx = await mount({ LSP_FAKE_DEF: 'null' }, {}, (registered) => { provider = registered })
if (provider === undefined) throw new Error('expected lsp-local to register a provider')
if (provider === undefined) throw new Error('expected lsp-stdio to register a provider')
const internals = provider as unknown as {
readonly instances: Map<string, { dispose(): Promise<void> }>
readonly queues: Map<string, Promise<void>>
@@ -376,7 +376,7 @@ describe('lsp-local end to end over a fake server', () => {
release.resolve(undefined)
await expect(disposing).rejects.toMatchObject({
errors: [firstFailure, secondFailure],
message: 'lsp-local instance teardown failed',
message: 'lsp-stdio instance teardown failed',
})
expect(internals.instances.size).toBe(0)
expect(internals.queues.size).toBe(0)
@@ -389,7 +389,7 @@ describe('lsp-local end to end over a fake server', () => {
const disposalErrors: unknown[] = []
ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
const providers: LspProvider[] = []
const register = ctx.lsp.registerProvider.bind(ctx.lsp)
@@ -446,7 +446,7 @@ describe('lsp-local end to end over a fake server', () => {
it('rejects at load when the command is not found', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, {
servers: {
@@ -3,11 +3,11 @@ import { chmod, mkdtemp, mkdir, rm, writeFile, realpath } from 'node:fs/promises
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import Lsp, { type LspQueryRequest } from '@deepseek-ai/dsh-lsp'
import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
import type { Config, LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-local'
import * as LspLocal from '@deepseek-ai/dsh-lsp-stdio'
import type { Config, LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-stdio'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
let root: string
@@ -33,7 +33,7 @@ function config(providerId: string, server: LspLocalServerConfig): Config {
return { servers: { [providerId]: server } }
}
describe('lsp-local provider resolution', () => {
describe('lsp-stdio provider resolution', () => {
it('resolves a bare command on the child PATH and registers the provider', async () => {
// A tiny executable script placed on a custom PATH dir: the load-time resolver must find it.
const bin = join(root, 'bin')
@@ -44,7 +44,7 @@ describe('lsp-local provider resolution', () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('onpath', {
command: 'fake-lsp',
@@ -58,7 +58,7 @@ describe('lsp-local provider resolution', () => {
it('skips empty PATH segments and fails when the command is absent', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('nope', {
command: 'fake-lsp',
@@ -73,7 +73,7 @@ describe('lsp-local provider resolution', () => {
// Use a server that never emits results and dispose the plugin, then confirm queries are refused.
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
// Grab the provider instance by registering, then dispose the whole plugin fiber.
const lsp = ctx.lsp
@@ -91,7 +91,7 @@ describe('lsp-local provider resolution', () => {
it('rejects a nonpositive teardown budget at load', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('bad-budget', {
command: process.execPath,
@@ -105,7 +105,7 @@ describe('lsp-local provider resolution', () => {
it('rejects a nonpositive byte cap at load', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('bad-cap', {
command: process.execPath,
@@ -119,7 +119,7 @@ describe('lsp-local provider resolution', () => {
it.each(['shutdownTimeoutMs', 'killGraceMs'] as const)('rejects %s above Node timer range at load', async (name) => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('bad-timer', {
command: process.execPath,
@@ -136,7 +136,7 @@ describe('lsp-local provider resolution', () => {
await writeFile(notExe, 'plain text, not executable')
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('abs-bad', {
command: notExe,
@@ -149,7 +149,7 @@ describe('lsp-local provider resolution', () => {
it('rejects an executable directory as a command at load', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('abs-directory', {
command: ws,
@@ -162,7 +162,7 @@ describe('lsp-local provider resolution', () => {
it('rejects an empty server table at load', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, { servers: {} })).rejects.toThrow(/servers must contain at least one server/)
await ctx.fiber.dispose()
@@ -171,7 +171,7 @@ describe('lsp-local provider resolution', () => {
it('rejects an empty server id at load', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, config('', {
command: process.execPath,
@@ -183,7 +183,7 @@ describe('lsp-local provider resolution', () => {
it('resolves every executable before publishing any provider', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, {
servers: {
@@ -198,7 +198,7 @@ describe('lsp-local provider resolution', () => {
it('waits for aborted sibling executable lookups before setup rejects', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
const slowStarted = Promise.withResolvers<undefined>()
const slowAborted = Promise.withResolvers<undefined>()
@@ -242,7 +242,7 @@ describe('lsp-local provider resolution', () => {
it('aborts executable resolution when disposed during setup', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
const subprocess = ctx.subprocess
const lookupStarted = Promise.withResolvers<AbortSignal>()
@@ -268,7 +268,7 @@ describe('lsp-local provider resolution', () => {
expect(signal.aborted).toBe(false)
const disposing = loading.dispose()
await expect(loading).rejects.toThrow('lsp-local setup disposed')
await expect(loading).rejects.toThrow('lsp-stdio setup disposed')
await expect(disposing).resolves.toBeUndefined()
expect(signal.aborted).toBe(true)
await ctx.fiber.dispose()
@@ -277,7 +277,7 @@ describe('lsp-local provider resolution', () => {
it('rolls back earlier registrations when a later server conflicts', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await expect(ctx.plugin(LspLocal, {
servers: {
@@ -6,8 +6,8 @@ import {
requestMethod,
supportsOperation,
supportsTransientOpen,
} from '@deepseek-ai/dsh-lsp-local'
import type { WireServerCapabilities } from '@deepseek-ai/dsh-lsp-local/src/protocol.ts'
} from '@deepseek-ai/dsh-lsp-stdio'
import type { WireServerCapabilities } from '@deepseek-ai/dsh-lsp-stdio/src/protocol.ts'
const RANGE = { start: { line: 1, character: 2 }, end: { line: 1, character: 5 } }
@@ -1,6 +1,6 @@
/**
* Keyless real-server e2e: drives the real `typescript-language-server` through the full
* `ctx.lsp` `dsh-lsp-local` stack over the base protocol, exercising all four operations. No API
* `ctx.lsp` `dsh-lsp-stdio` stack over the base protocol, exercising all four operations. No API
* key needed the server is a local dev dependency. This establishes one compatibility floor
* (TypeScript), not a cross-language claim.
*/
@@ -10,10 +10,10 @@ import { mkdtemp, mkdir, rm, writeFile, realpath } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import Lsp, { type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp'
import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
import * as LspLocal from '@deepseek-ai/dsh-lsp-stdio'
// The server binary is a dev dependency of this package; resolve its pnpm-hoisted .bin path.
const serverBin = join(
@@ -54,7 +54,7 @@ beforeAll(async () => {
ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await ctx.plugin(LspLocal, {
servers: {
@@ -36,7 +36,7 @@
"path": "../../subprocess/subprocess"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/lsp/README.md
README.md: 1a4b43a157e97e88fb93a357cc2ea1ac71006a78
README.zh.md: 9a0fb75ca392bc1ba3e30f2ea9261bb1d14dbde4
README.md: 910b7e06ac849bbf3b56d49c1b6258a3633c6bcc
README.zh.md: 519964cd543005b9db0b345d1632a76bf56d6b36
+1 -1
View File
@@ -9,7 +9,7 @@ This package owns the Service Definition role of the LSP capability:
| Package | Role |
|---|---|
| `@deepseek-ai/dsh-lsp` (this) | Service Definition: the service, provider registry keyed by branded id + extension mapping, per-query selection, request/result vocabulary, the `LspError` taxonomy |
| `@deepseek-ai/dsh-lsp-local` | Service provider: a generic local backend that registers configured stdio language-server providers |
| `@deepseek-ai/dsh-lsp-stdio` | Service provider: a generic local backend that registers configured stdio language-server providers |
| `@deepseek-ai/dsh-tool-lsp` | Consumer: the model-facing `lsp` tool over `ctx.lsp` |
The seam exposes exactly four semantic operations — `goToDefinition`, `findReferences`, `goToImplementation`, `hover` — and no generic JSON-RPC escape hatch, so no protocol payload or unreviewed command/mutation reaches a provider through `ctx.lsp`.
+1 -1
View File
@@ -9,7 +9,7 @@
| 包 | 职责 |
|---|---|
| `@deepseek-ai/dsh-lsp`(本包) | Service Definition:服务、以品牌化 id + 扩展名映射为 key 的提供方注册表、逐查询选择、请求/结果词汇、`LspError` 分类体系 |
| `@deepseek-ai/dsh-lsp-local` | Service provider:通用本地后端,注册已配置的 stdio 语言服务器提供方 |
| `@deepseek-ai/dsh-lsp-stdio` | Service provider:通用本地后端,注册已配置的 stdio 语言服务器提供方 |
| `@deepseek-ai/dsh-tool-lsp` | Consumer:面向模型的 `lsp` 工具,基于 `ctx.lsp` |
该 seam 恰好公开四种语义操作:`goToDefinition``findReferences``goToImplementation``hover`,且没有通用 JSON-RPC 逃生口,因此任何协议载荷或未经评审的命令/修改都无法通过 `ctx.lsp` 到达提供方。
+1 -1
View File
@@ -21,7 +21,7 @@
"path": "../../llm/llm"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/tool-lsp/README.md
README.md: 36fa3026cba3fc692ae5a86c7307acf77563cf53
README.zh.md: 2372ee2501bcd6b659fe7fe984c00da632c8d0bc
README.md: e0fd5738cf114314a8483125d20142d5f1ffac40
README.zh.md: 8c908c1e8766ab47e9cefbc9d1f72f05e8c2709f
+1 -1
View File
@@ -18,7 +18,7 @@ The tool requires the workspace root from the session `header.cwd`, with no fall
|---|---|---|
| `maxLocations` | `100` | Largest number of rendered locations before an omission marker. |
| `maxResultChars` | `16000` | Largest complete rendered result, including truncation metadata. |
| `timeoutMs` | `60000` | Tool-call timeout budget, enforced by `dsh-timeout-policy`; covers the complete queued open/query/close lifecycle and is not model-configurable. |
| `timeoutMs` | `60000` | Tool-call timeout budget, enforced by `dsh-tool-call-timeout-policy`; covers the complete queued open/query/close lifecycle and is not model-configurable. |
## Model Experience
+1 -1
View File
@@ -18,7 +18,7 @@ Namespace 插件(`name``inject``Config``apply`,无默认导出)
|---|---|---|
| `maxLocations` | `100` | 出现省略标记前可渲染位置的最大数量。 |
| `maxResultChars` | `16000` | 完整渲染结果的最大长度,包括截断元数据。 |
| `timeoutMs` | `60000` | 由 `dsh-timeout-policy` 强制执行的工具调用超时预算;覆盖完整的排队打开/查询/关闭生命周期,且模型不可配置。 |
| `timeoutMs` | `60000` | 由 `dsh-tool-call-timeout-policy` 强制执行的工具调用超时预算;覆盖完整的排队打开/查询/关闭生命周期,且模型不可配置。 |
## 模型体验
+2 -2
View File
@@ -49,12 +49,12 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-lsp": "workspace:^",
"@deepseek-ai/dsh-lsp-local": "workspace:^",
"@deepseek-ai/dsh-lsp-stdio": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
+1 -1
View File
@@ -3,7 +3,7 @@
* (`goToDefinition`/`findReferences`/`goToImplementation`/`hover`); it converts one-based UTF-16
* cursor coordinates to the seam's zero-based positions, requires the session workspace with no
* fallback, caps and renders results, and attaches a configurable timeout budget for
* `dsh-timeout-policy` to enforce. It runtime-injects only `tools`, `lsp`, and `systemPrompt` and
* `dsh-tool-call-timeout-policy` to enforce. It runtime-injects only `tools`, `lsp`, and `systemPrompt` and
* imports no provider.
*
* Namespace plugin (named exports, no default export).
@@ -5,12 +5,12 @@ import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import Lsp from '@deepseek-ai/dsh-lsp'
import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
import * as TimeoutPolicy from '@deepseek-ai/dsh-timeout-policy'
import * as LspLocal from '@deepseek-ai/dsh-lsp-stdio'
import * as TimeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy'
import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
/**
@@ -49,10 +49,10 @@ function serverScript(hang: boolean): string {
async function mount(hang: boolean, timeoutMs?: number): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(Lsp)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(LspLocal, {
servers: {
inline: {
+2 -2
View File
@@ -3,7 +3,7 @@ import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import Lsp, { LspProviderId, type LspProvider, type LspProviderQuery, type LspQueryResult } from '@deepseek-ai/dsh-lsp'
import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
import { DEFAULT_LSP_TOOL_TIMEOUT_MS, LSP_PROMPT_TEXT } from '@deepseek-ai/dsh-tool-lsp'
@@ -33,7 +33,7 @@ async function mount(
): Promise<{ ctx: Context }> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(Lsp)
if (provider) (ctx.lsp as Lsp).registerProvider(provider)
await ctx.plugin(ToolLsp, config)
+1 -1
View File
@@ -33,7 +33,7 @@
"path": "../lsp"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}