@deepseek-ai/dsh-lsp 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/lsp/lsp/README.md
5
+ README.md: bab312e73f746d0d782ab5771429fed1397255aa
6
+ README.zh.md: 7ef95c725eba4a35f99014b446fc5648d8aab20a
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @deepseek-ai/dsh-lsp
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ The **LSP capability seam**: an abstract `LspService` (`ctx.lsp`) defining WHAT semantic code navigation the harness has — go to definition, find references, find implementations, hover — over language-server providers, without binding the model contract to local subprocesses.
6
+
7
+ This package owns the Service Definition role of the LSP capability:
8
+
9
+ | Package | Role |
10
+ |---|---|
11
+ | `@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 |
12
+ | `@deepseek-ai/dsh-lsp-local` | Service provider: a generic local backend that registers configured stdio language-server providers |
13
+ | `@deepseek-ai/dsh-tool-lsp` | Consumer: the model-facing `lsp` tool over `ctx.lsp` |
14
+
15
+ 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`.
16
+
17
+ ## Service API (`ctx.lsp`)
18
+
19
+ | Member | Semantics |
20
+ |---|---|
21
+ | `registerProvider(provider)` | Register a backend, atomically reserving its branded `id` and every normalized file extension. Any invalid input or conflict publishes nothing and throws `LspError` (`LSP_INVALID_PROVIDER` / `LSP_CONFLICT`). Returns a disposer releasing all reservations. Disposed with the calling fiber. |
22
+ | `query(request, signal?)` | Select the provider by the file's final extension, derive the `languageId` from that provider's mapping, and run one query. No match throws `LspError` `LSP_UNAVAILABLE`. |
23
+
24
+ Selection is per query and order-independent: a provider owns a set of extensions exclusively, so registration and HMR order never change routing. Extension keys normalize to lowercase, leading-dot form; the `languageId` only synchronizes the transient document, never participates in selection. The first version has no glob, language-id, or explicit route selector.
25
+
26
+ Providers register **capabilities**, not tools. `dsh-tool-lsp` is the only owner of the model-facing name, description, prompt guidance, schema, and presentation.
27
+
28
+ ## Vocabulary
29
+
30
+ `LspQueryRequest` (`operation`, `filePath`, `position`, `workspaceRoot`) — every field required, so no field needs implementation defaulting and there is no `resolve()` step. Positions and ranges are zero-based UTF-16, matching the protocol; the tool owns the one-based cursor convention. `findReferences` always includes declarations — providers enforce this internally, so callers get no flag. `LspQueryResult` is a CLOSED discriminated union: `{ kind: 'locations'; locations; resolvedWorkspaceUri }` for navigation, `{ kind: 'hover'; hover }` for hover (content or `null`) — consumers `switch` to exhaustiveness so a new arm breaks compilation until handled. `resolvedWorkspaceUri` is the provider's canonical workspace `file:` URI; callers relativize location URIs against it instead of applying host-platform path rules to the possibly symlinked request root. See `src/types.ts` for the full contracts and `src/index.ts` for the `LspError` codes, including `LSP_DISPOSED` and `LSP_MALFORMED_RESPONSE`.
31
+
32
+ ## Model Experience
33
+
34
+ Indirectly, through `dsh-tool-lsp`, which owns the model-facing `lsp` schema, prompt, and rendered results while this registry contributes no prompt or schema itself.
35
+
36
+ #### KV Cache effect
37
+
38
+ No direct invalidation; `dsh-tool-lsp` owns request-prefix changes.
39
+
40
+ ## Known Limitations and Deferred Work
41
+
42
+ - **Exclusive extension ownership within one runtime** — two providers cannot both claim `.ts`, even with different language ids; overlaps fail registration. The intended extension is a deployment-configured selector above registrations, which can relax exclusive reservation without adding provider choice to model input ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md)).
43
+ - **Four operations only** — symbols and call hierarchy are deferred (they need different schemas); diagnostics need separate freshness/accumulation rules; mutations (rename, code actions, formatting) require separate tools with preview, permission, and write-policy integration.
44
+ - **No observation surface** — availability is observed only by running `query()` and routing the thrown `LspError` codes; there is no provider-change event or capability-status query.
package/README.zh.md ADDED
@@ -0,0 +1,44 @@
1
+ # @deepseek-ai/dsh-lsp
2
+
3
+ [English](README.md) | 中文
4
+
5
+ **LSP 能力 seam**:抽象 `LspService`(`ctx.lsp`)定义 harness 具备哪些语义代码导航能力(转到定义、查找引用、查找实现、悬停),并通过语言服务器提供方实现,不把模型约定绑定到本地子进程。
6
+
7
+ 本包承担 LSP 能力的 Service Definition 角色:
8
+
9
+ | 包 | 职责 |
10
+ |---|---|
11
+ | `@deepseek-ai/dsh-lsp`(本包) | Service Definition:服务、以品牌化 id + 扩展名映射为 key 的提供方注册表、逐查询选择、请求/结果词汇、`LspError` 分类体系 |
12
+ | `@deepseek-ai/dsh-lsp-local` | Service provider:通用本地后端,注册已配置的 stdio 语言服务器提供方 |
13
+ | `@deepseek-ai/dsh-tool-lsp` | Consumer:面向模型的 `lsp` 工具,基于 `ctx.lsp` |
14
+
15
+ 该 seam 恰好公开四种语义操作:`goToDefinition`、`findReferences`、`goToImplementation`、`hover`,且没有通用 JSON-RPC 逃生口,因此任何协议载荷或未经评审的命令/修改都无法通过 `ctx.lsp` 到达提供方。
16
+
17
+ ## 服务 API(`ctx.lsp`)
18
+
19
+ | 成员 | 语义 |
20
+ |---|---|
21
+ | `registerProvider(provider)` | 注册后端,以原子方式保留其品牌化 `id` 与每个规范化文件扩展名。任何无效输入或冲突都不会发布内容,并抛出 `LspError`(`LSP_INVALID_PROVIDER`/`LSP_CONFLICT`)。返回释放所有保留项的 disposer。随调用 fiber 释放。 |
22
+ | `query(request, signal?)` | 按文件最终扩展名选择提供方,从该提供方的映射派生 `languageId`,并运行一次查询。没有匹配项时抛出 `LspError` `LSP_UNAVAILABLE`。 |
23
+
24
+ 选择逐查询进行且与顺序无关:一个提供方独占一组扩展名,因此注册和 HMR 顺序绝不会改变路由。扩展名 key 规范化为小写且以点开头;`languageId` 只用于同步临时文档,绝不参与选择。第一版没有 glob、language-id 或显式路由 selector。
25
+
26
+ 提供方注册的是**能力** 而非工具。`dsh-tool-lsp` 是面向模型名称、描述、提示词指引、schema 和呈现的唯一 owner。
27
+
28
+ ## 词汇
29
+
30
+ `LspQueryRequest`(`operation`、`filePath`、`position`、`workspaceRoot`):每个字段都必填,因此没有字段需要实现默认值,也不存在 `resolve()` 步骤。位置与范围使用从零开始的 UTF-16,与协议一致;工具拥有从 1 开始的光标约定。`findReferences` 始终包含声明,提供方在内部强制执行,因此调用方没有 flag。`LspQueryResult` 是封闭的判别联合:导航使用 `{ kind: 'locations'; locations; resolvedWorkspaceUri }`,悬停使用 `{ kind: 'hover'; hover }`(内容或 `null`);消费方通过 `switch` 实现穷尽检查,因此新增分支会使编译失败,直到完成处理。`resolvedWorkspaceUri` 是提供方的规范工作区 `file:` URI;调用方相对化位置 URI 时以它为基准,而不是对可能含符号链接的请求根应用宿主平台路径规则。完整约定见 `src/types.ts`;`src/index.ts` 给出 `LspError` code,包括 `LSP_DISPOSED` 和 `LSP_MALFORMED_RESPONSE`。
31
+
32
+ ## 模型体验
33
+
34
+ 通过 `dsh-tool-lsp` 间接影响;该工具拥有面向模型的 `lsp` schema、提示词与渲染结果,本注册表自身不贡献提示词或 schema。
35
+
36
+ #### KV Cache 影响
37
+
38
+ 不会直接失效;请求前缀变更由 `dsh-tool-lsp` 负责。
39
+
40
+ ## 已知限制与暂缓事项
41
+
42
+ - **同一运行时内扩展名归属互斥**:两个提供方不能同时声明 `.ts`,即使 language id 不同;重叠会使注册失败。预期扩展是在注册之上增加部署配置的 selector;它可以放宽互斥保留,而无需把提供方选择加入模型输入(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md))。
43
+ - **仅四种操作**:symbol 与 call hierarchy 暂缓(它们需要不同 schema);diagnostics 需要独立的新鲜度/累积规则;修改操作(rename、code action、formatting)需要独立工具,并集成预览、权限和写入策略。
44
+ - **没有观测表层**:可用性只能通过运行 `query()` 并按抛出的 `LspError` code 路由来观测;没有提供方变更事件或能力状态查询。
package/lib/index.js ADDED
@@ -0,0 +1,110 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { HarnessError } from "@deepseek-ai/dsh-llm";
3
+ //#region lib/types/brand.js
4
+ /**
5
+ * dsh-lsp's owned branded id: {@link LspProviderId}, the opaque identity a provider reserves on
6
+ * `ctx.lsp`. The `Branded<B>` primitive lives in `@deepseek-ai/dsh-brand`; keeping the type and its
7
+ * factory together here lets `index.ts` re-export both under one name.
8
+ * @module @deepseek-ai/dsh-lsp/brand
9
+ */
10
+ /**
11
+ * Brand a string as an {@link LspProviderId}. No validation — the registry rejects an empty id at
12
+ * registration.
13
+ * @param id - the provider's stable identifier.
14
+ * @returns the same string, branded.
15
+ */
16
+ function LspProviderId(id) {
17
+ return id;
18
+ }
19
+ //#endregion
20
+ //#region lib/types/index.js
21
+ /**
22
+ * Service Definition for the LSP capability seam (`ctx.lsp`): a language-server provider registry and per-query,
23
+ * order-independent selection over normalized goToDefinition/findReferences/goToImplementation/
24
+ * hover queries.
25
+ *
26
+ * A provider reserves a branded id and an exclusive set of file extensions atomically:
27
+ * {@link Lsp.registerProvider} validates and conflict-checks everything before mutating, so an
28
+ * invalid or conflicting registration publishes nothing, and its disposer releases every
29
+ * reservation together. Selection routes a query by the file's final extension; it never depends on
30
+ * registration order. The seam exposes exactly the four operations and no JSON-RPC escape hatch.
31
+ * @module @deepseek-ai/dsh-lsp
32
+ */
33
+ /**
34
+ * Structured LSP failure. Extends {@link HarnessError} with a stable `code`
35
+ * (`LSP_INVALID_PROVIDER`, `LSP_CONFLICT`, `LSP_UNAVAILABLE`, `LSP_DISPOSED`,
36
+ * `LSP_UNSUPPORTED_OPERATION`, `LSP_MALFORMED_RESPONSE`, …) that callers route on instead of
37
+ * parsing `message`.
38
+ */
39
+ var LspError = class extends HarnessError {};
40
+ /**
41
+ * Extract a file's final extension as a normalized, lowercase, leading-dot key (e.g. `Foo.TS` →
42
+ * `.ts`, `foo.d.ts` → `.ts`). Returns `''` for a name with no extension or a leading-dot dotfile
43
+ * (`.bashrc`), which no route ever matches. Splits on both `/` and `\` so a caller's path separator
44
+ * does not change the result.
45
+ * @param filePath - the source path to inspect.
46
+ * @returns the normalized extension, or `''` when there is none.
47
+ */
48
+ function finalExtension(filePath) {
49
+ const lastSlash = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
50
+ const base = lastSlash >= 0 ? filePath.slice(lastSlash + 1) : filePath;
51
+ const dot = base.lastIndexOf(".");
52
+ if (dot <= 0) return "";
53
+ return base.slice(dot).toLowerCase();
54
+ }
55
+ /** A well-formed normalized extension: a dot followed by one or more non-dot, non-separator chars. */
56
+ const EXTENSION_PATTERN = /^\.[^./\\]+$/;
57
+ /**
58
+ * `ctx.lsp`. Holds the id reservations and the extension→route table; both are populated and cleared
59
+ * together per provider so a route always has a live provider.
60
+ */
61
+ var Lsp = class extends Service {
62
+ providerIds = /* @__PURE__ */ new Set();
63
+ routes = /* @__PURE__ */ new Map();
64
+ constructor(ctx) {
65
+ super(ctx, "lsp");
66
+ }
67
+ registerProvider(provider) {
68
+ const id = provider.id;
69
+ if (id.trim() === "") throw new LspError("an LSP provider id must be a non-empty string", "LSP_INVALID_PROVIDER");
70
+ if (this.providerIds.has(id)) throw new LspError(`an LSP provider with id "${id}" is already registered`, "LSP_CONFLICT");
71
+ const entries = Object.entries(provider.extensionToLanguage);
72
+ if (entries.length === 0) throw new LspError(`LSP provider "${id}" registers no file extensions`, "LSP_INVALID_PROVIDER");
73
+ const pending = /* @__PURE__ */ new Map();
74
+ for (const [rawExt, languageId] of entries) {
75
+ const ext = normalizeExtension(rawExt);
76
+ if (!EXTENSION_PATTERN.test(ext)) throw new LspError(`LSP provider "${id}" maps an invalid extension "${rawExt}"`, "LSP_INVALID_PROVIDER");
77
+ if (languageId.trim() === "") throw new LspError(`LSP provider "${id}" maps extension "${ext}" to an empty language id`, "LSP_INVALID_PROVIDER");
78
+ if (pending.has(ext)) throw new LspError(`LSP provider "${id}" maps extension "${ext}" more than once`, "LSP_INVALID_PROVIDER");
79
+ pending.set(ext, {
80
+ provider,
81
+ languageId
82
+ });
83
+ }
84
+ for (const ext of pending.keys()) if (this.routes.has(ext)) throw new LspError(`extension "${ext}" is already handled by another LSP provider`, "LSP_CONFLICT");
85
+ const dispose = this.ctx.effect(function* () {
86
+ this.providerIds.add(id);
87
+ for (const [ext, route] of pending) this.routes.set(ext, route);
88
+ yield () => {
89
+ this.providerIds.delete(id);
90
+ for (const ext of pending.keys()) this.routes.delete(ext);
91
+ };
92
+ }.bind(this), "lsp.registerProvider()");
93
+ return () => void dispose();
94
+ }
95
+ async query(request, signal) {
96
+ const route = this.routes.get(finalExtension(request.filePath));
97
+ if (route === void 0) throw new LspError(`no LSP provider handles "${request.filePath}"`, "LSP_UNAVAILABLE");
98
+ return route.provider.query({
99
+ ...request,
100
+ languageId: route.languageId
101
+ }, signal);
102
+ }
103
+ };
104
+ /** Lowercase an extension and ensure it carries a leading dot; `EXTENSION_PATTERN` rejects the rest. */
105
+ function normalizeExtension(ext) {
106
+ const lower = ext.toLowerCase();
107
+ return lower.startsWith(".") ? lower : `.${lower}`;
108
+ }
109
+ //#endregion
110
+ export { Lsp, Lsp as default, LspError, LspProviderId, finalExtension };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-lsp`.
4
+ * @module @deepseek-ai/dsh-lsp/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-lsp";
7
+ /** Cordis companion plugin name. */
8
+ const name = "lsp-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: provider ids and extension routes are private, atomically updated state;
13
+ * the seam exposes neither an enumerable snapshot nor lifecycle events to compare independently.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * dsh-lsp's owned branded id: {@link LspProviderId}, the opaque identity a provider reserves on
3
+ * `ctx.lsp`. The `Branded<B>` primitive lives in `@deepseek-ai/dsh-brand`; keeping the type and its
4
+ * factory together here lets `index.ts` re-export both under one name.
5
+ * @module @deepseek-ai/dsh-lsp/brand
6
+ */
7
+ import type { Branded } from '@deepseek-ai/dsh-brand';
8
+ /** Opaque provider identity, reserved atomically with its extension mappings at registration. */
9
+ export type LspProviderId = Branded<'LspProviderId'>;
10
+ /**
11
+ * Brand a string as an {@link LspProviderId}. No validation — the registry rejects an empty id at
12
+ * registration.
13
+ * @param id - the provider's stable identifier.
14
+ * @returns the same string, branded.
15
+ */
16
+ export declare function LspProviderId(id: string): LspProviderId;
17
+ //# sourceMappingURL=brand.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Service Definition for the LSP capability seam (`ctx.lsp`): a language-server provider registry and per-query,
3
+ * order-independent selection over normalized goToDefinition/findReferences/goToImplementation/
4
+ * hover queries.
5
+ *
6
+ * A provider reserves a branded id and an exclusive set of file extensions atomically:
7
+ * {@link Lsp.registerProvider} validates and conflict-checks everything before mutating, so an
8
+ * invalid or conflicting registration publishes nothing, and its disposer releases every
9
+ * reservation together. Selection routes a query by the file's final extension; it never depends on
10
+ * registration order. The seam exposes exactly the four operations and no JSON-RPC escape hatch.
11
+ * @module @deepseek-ai/dsh-lsp
12
+ */
13
+ import { Context, Service } from '@deepseek-ai/cordis';
14
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
15
+ import type { LspProvider, LspQueryRequest, LspQueryResult, LspService } from './types.ts';
16
+ export { LspProviderId } from './brand.ts';
17
+ export type { LspHover, LspLocation, LspOperation, LspPosition, LspProvider, LspProviderQuery, LspQueryRequest, LspQueryResult, LspRange, LspService, } from './types.ts';
18
+ declare module '@deepseek-ai/cordis' {
19
+ interface Context {
20
+ lsp: LspService;
21
+ }
22
+ }
23
+ /**
24
+ * Structured LSP failure. Extends {@link HarnessError} with a stable `code`
25
+ * (`LSP_INVALID_PROVIDER`, `LSP_CONFLICT`, `LSP_UNAVAILABLE`, `LSP_DISPOSED`,
26
+ * `LSP_UNSUPPORTED_OPERATION`, `LSP_MALFORMED_RESPONSE`, …) that callers route on instead of
27
+ * parsing `message`.
28
+ */
29
+ export declare class LspError extends HarnessError {
30
+ }
31
+ /**
32
+ * Extract a file's final extension as a normalized, lowercase, leading-dot key (e.g. `Foo.TS` →
33
+ * `.ts`, `foo.d.ts` → `.ts`). Returns `''` for a name with no extension or a leading-dot dotfile
34
+ * (`.bashrc`), which no route ever matches. Splits on both `/` and `\` so a caller's path separator
35
+ * does not change the result.
36
+ * @param filePath - the source path to inspect.
37
+ * @returns the normalized extension, or `''` when there is none.
38
+ */
39
+ export declare function finalExtension(filePath: string): string;
40
+ /**
41
+ * `ctx.lsp`. Holds the id reservations and the extension→route table; both are populated and cleared
42
+ * together per provider so a route always has a live provider.
43
+ */
44
+ export declare class Lsp extends Service implements LspService {
45
+ private readonly providerIds;
46
+ private readonly routes;
47
+ constructor(ctx: Context);
48
+ registerProvider(provider: LspProvider): () => void;
49
+ query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>;
50
+ }
51
+ export default Lsp;
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-lsp`.
3
+ * @module @deepseek-ai/dsh-lsp/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "lsp-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * LSP seam vocabulary: the normalized request, provider, and result contracts. Types only — the
3
+ * {@link LspError} taxonomy and the {@link LspProviderId} brand factory are runtime and live in
4
+ * `index.ts`. Positions and ranges are zero-based UTF-16, matching the protocol; the model-facing
5
+ * tool owns the one-based cursor convention. The seam exposes no protocol types, process or document
6
+ * controls, or generic JSON-RPC escape hatch — only the four semantic operations.
7
+ * @module @deepseek-ai/dsh-lsp/types
8
+ */
9
+ import type { LspProviderId } from './brand.ts';
10
+ /**
11
+ * The four semantic queries the seam and model expose. A closed union: adding an operation is a
12
+ * compile-enforced change across the seam, providers, and the tool. Symbols and call hierarchy are
13
+ * not operations here; they need different schemas.
14
+ */
15
+ export type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover';
16
+ /** A zero-based UTF-16 cursor coordinate, matching the LSP wire convention. */
17
+ export interface LspPosition {
18
+ /** Zero-based line. */
19
+ readonly line: number;
20
+ /** Zero-based UTF-16 code-unit offset within the line. */
21
+ readonly character: number;
22
+ }
23
+ /** A zero-based UTF-16 half-open range `[start, end)`. */
24
+ export interface LspRange {
25
+ readonly start: LspPosition;
26
+ readonly end: LspPosition;
27
+ }
28
+ /**
29
+ * A caller's normalized query. Every field is required: `workspaceRoot` is caller-supplied,
30
+ * `languageId` comes from the provider registration (not here), and consumers own timeouts and
31
+ * result limits — so no field needs implementation defaulting and there is no `resolve()` step.
32
+ */
33
+ export interface LspQueryRequest {
34
+ /** Which semantic query to run. */
35
+ readonly operation: LspOperation;
36
+ /** The source file to query (relative to `workspaceRoot` or absolute; the provider canonicalizes). */
37
+ readonly filePath: string;
38
+ /** The zero-based UTF-16 cursor position to query at. */
39
+ readonly position: LspPosition;
40
+ /** The workspace root the provider resolves against and indexes; required, never defaulted. */
41
+ readonly workspaceRoot: string;
42
+ }
43
+ /**
44
+ * A request as a provider receives it: the caller's {@link LspQueryRequest} plus the `languageId`
45
+ * the seam derived from the provider's extension mapping. The language id only synchronizes the
46
+ * transient document; it does not participate in selection.
47
+ */
48
+ export interface LspProviderQuery extends LspQueryRequest {
49
+ /** The LSP language id for `filePath`, from this provider's extension mapping. */
50
+ readonly languageId: string;
51
+ }
52
+ /** One resolved location: a document URI and the range within it. */
53
+ export interface LspLocation {
54
+ /** The target document URI (`file:` or otherwise), verbatim from the server. */
55
+ readonly uri: string;
56
+ /** The range within the target document. */
57
+ readonly range: LspRange;
58
+ }
59
+ /** Normalized hover content, or `null` for no hover at the position. */
60
+ export interface LspHover {
61
+ /** The normalized hover text (markdown or plaintext, provider-joined). */
62
+ readonly contents: string;
63
+ /** The range the hover applies to, when the server supplied one. */
64
+ readonly range?: LspRange;
65
+ }
66
+ /**
67
+ * The closed result union. Navigation operations (`goToDefinition`, `findReferences`,
68
+ * `goToImplementation`) normalize to `locations`; `hover` normalizes to content or `null`.
69
+ * Consumers `switch` on `kind` to exhaustiveness so a new arm breaks compilation until handled.
70
+ *
71
+ * The `locations` variant carries `resolvedWorkspaceUri`: the provider's canonical `file:` URI for
72
+ * the request's workspace root. A caller that relativizes location URIs MUST use this, not parse the
73
+ * request's possibly symlinked process path with host-platform rules; the execution platform may
74
+ * differ from the caller's.
75
+ */
76
+ export type LspQueryResult = {
77
+ readonly kind: 'locations';
78
+ readonly locations: readonly LspLocation[];
79
+ readonly resolvedWorkspaceUri: string;
80
+ } | {
81
+ readonly kind: 'hover';
82
+ readonly hover: LspHover | null;
83
+ };
84
+ /**
85
+ * A language-server backend registered on `ctx.lsp`. Each provider owns a stable {@link
86
+ * LspProviderId} and an extension-to-language-id map (lowercase, leading-dot keys).
87
+ * `findReferences` always includes declarations — the provider enforces this internally; callers
88
+ * get no flag.
89
+ */
90
+ export interface LspProvider {
91
+ /** Stable provider identity, reserved atomically with the extension mappings. */
92
+ readonly id: LspProviderId;
93
+ /** Lowercase leading-dot extension → LSP language id (e.g. `{ '.ts': 'typescript' }`). */
94
+ readonly extensionToLanguage: Readonly<Record<string, string>>;
95
+ /**
96
+ * Run one query. The seam has already selected this provider and derived `languageId`.
97
+ * @param request - the resolved provider query (caller request + derived language id).
98
+ * @param signal - optional cancellation; the provider stops its own work when it aborts.
99
+ * @returns the normalized, closed-union result.
100
+ */
101
+ query(request: LspProviderQuery, signal?: AbortSignal): Promise<LspQueryResult>;
102
+ }
103
+ /**
104
+ * The LSP capability seam (`ctx.lsp`). Owns provider registration/selection and normalized query
105
+ * execution; exposes exactly the four operations and no protocol escape hatch.
106
+ */
107
+ export interface LspService {
108
+ /**
109
+ * Register a provider, atomically reserving its id and every normalized extension. Any conflict
110
+ * or invalid input publishes nothing and throws `LspError`; the returned disposer releases all
111
+ * reservations. Disposed with the calling fiber.
112
+ * @param provider - the backend to register.
113
+ * @returns a synchronous disposer releasing the id and all extension reservations.
114
+ */
115
+ registerProvider(provider: LspProvider): () => void;
116
+ /**
117
+ * Select a provider by the file's extension and run one query. Selection is per-query and
118
+ * order-independent; no match throws `LspError` `LSP_UNAVAILABLE`.
119
+ * @param request - the normalized query.
120
+ * @param signal - optional cancellation forwarded to the selected provider.
121
+ * @returns the normalized, closed-union result.
122
+ */
123
+ query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>;
124
+ }
125
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-lsp",
3
+ "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/lsp/lsp"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
38
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
39
+ },
40
+ "devDependencies": {
41
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
42
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
43
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
44
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.1"
45
+ }
46
+ }