lspz 0.10.2

AI-friendly LSP compression proxy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# lspz 压缩格式规范

**版本**: v0.1.0
**状态**: 定稿
**最后更新**: 2026-05-09

## 概述

本文档定义 lspz 使用的紧凑格式(Compact Format),用于压缩 LSP 诊断消息,减少 Token 消耗。

### 目标

- **Token 节省**: 相比原始 JSON 格式节省 ≥ 40% Token
- **语义完整**: 保留所有关键信息(文件、行号、严重级别、消息)
- **可还原**: Agent 端可以还原为标准 LSP Diagnostic 格式
- **可扩展**: 支持版本控制和向后兼容

---

## 调研发现: 为什么去重是最核心的压缩手段

### 真实 LSP 服务器的字段使用情况

对 3 个真实 LSP 服务器(jinja-lsp、typescript-language-server、yaml-language-server)的实际诊断输出分析:

| 字段 | jinja-lsp | typescript-LS | yaml-LS | 可裁剪? |
|------|-----------|---------------|---------|---------|
| `range` |||| ❌ (核心信息) |
| `message` |||| ❌ (核心信息) |
| `severity` |||| ❌ (核心信息) |
| `source` | ✅ (固定"jinja-lsp") || ✅ (固定"YAML") | ✅ (AI 不需要) |
| `code` |||| ⚠️ 有时有用 |
| `relatedInformation` |||| ✅ (AI 很少需要) |
| `tags` || ✅ (Unnecessary/Deprecated) || ⚠️ 可压缩 |
| `data` |||| ✅ (极少使用) |
| `codeDescription` |||| ✅ (极少使用) |

**结论**: 字段裁剪带来的收益有限——大多数服务器只使用 4-6 个字段。`dedup` 才是收益最大的策略。

### OpenCode 的现有压缩策略

作为参考的 AI Coding Agent OpenCode 的处理方式:

- 只保留 `severity === 1` (ERROR),丢弃 WARNING / INFO / HINT
- 每个文件最多 20 条,超出部分用 `"... and N more"` 替代
- 格式化为 `"ERROR [L:C] msg"` 单行字符串
- 工具输出截断在 2000 字符/50KB

这意味着:
1. OpenCode 丢弃了**所有警告信息**——这是 lspz 可以保留的信号
2. 每文件 20 条的上限在大型重构时会丢失信息——lspz 可以用紧凑格式保留更多
3. 没有跨文件错误分组——lspz 可以提供结构化分类

### lspz 的差异化价值

| 维度 | OpenCode | lspz |
|------|----------|------|
| WARNING | ❌ 丢弃 | ✅ 保留为 `W` 单字符标识 |
| 每文件上限 | 20 条 | 无上限(紧凑格式) |
| 跨文件聚合 | ❌ 无 | ✅ 按 (message, code) 分组 |
| 输出格式 | 非结构化文本 | 结构化 CompactJSON |
| 标准化 | OpenCode 独有 | 所有 Agent 通用 |

---

## 压缩策略(按优先级排列)

### Strategy 1: 去重合并 (Deduplication) — #1 收益

**为什么是 #1**: 一个 TypeScript 文件可能包含 30 个 "unused variable" warnings——它们的 message 和 severity 完全相同,只有 range 不同。合并后从 30 个 JSON 对象变成 1 个 entry + 30 个 compact range。

**规则**: 同一文件中相同 `message` + `severity` + `code` 的诊断合并为一个条目,`ranges` 平铺。

```json
// 原始: 3 条重复诊断 → 约 150 tokens
[
  {"range": {"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 5}}, "severity": 2, "message": "unused variable `x`"},
  {"range": {"start": {"line": 15, "character": 0}, "end": {"line": 15, "character": 5}}, "severity": 2, "message": "unused variable `x`"},
  {"range": {"start": {"line": 20, "character": 0}, "end": {"line": 20, "character": 5}}, "severity": 2, "message": "unused variable `x`"}
]

// 压缩后: 1 条 + 3 个 range → 约 30 tokens (80% 节省!)
{
  "m": "unused variable `x`",
  "s": "W",
  "c": "unused_variable",
  "r": [[10, 0, 10, 5], [15, 0, 15, 5], [20, 0, 20, 5]]
}
```

#### 去重算法 (HashMap 分组)

```mermaid
flowchart LR
    subgraph 输入["原始 Diagnostic[]"]
        D1["{msg:'unused x', severity:2, code:'unused_var', range:{10,0,10,5}}"]
        D2["{msg:'unused x', severity:2, code:'unused_var', range:{15,0,15,5}}"]
        D3["{msg:'unused x', severity:2, code:'unused_var', range:{20,0,20,5}}"]
        D4["{msg:'undeclared y', severity:1, code:'E0425', range:{25,0,25,8}}"]
    end

    subgraph 分组["HashMap 分组"]
        K1["key = hash(message, severity, code)"]
        K2["k1: (unused x, 2, unused_var)"]
        K3["k2: (undeclared y, 1, E0425)"]
    end

    subgraph 合并["同一 key 内合并"]
        M1["Group k1:{ranges: [10:0-10:5, 15:0-15:5, 20:0-20:5], count: 3}"]
        M2["Group k2:{ranges: [25:0-25:8], count: 1}"]
    end

    subgraph 输出["去重结果"]
        O1["1 条 compressed entry<br/>(取代原始 3 条)"]
        O2["1 条 compressed entry<br/>(未去重)"]
    end

    D1 & D2 & D3 & D4 --> K1 --> K2 & K3
    K2 --> M1 --> O1
    K3 --> M2 --> O2

    classDef input fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
    classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
    classDef output fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff

    class D1,D2,D3,D4 input
    class K1,K2,K3,M1,M2 step
    class O1,O2 output
```

**关键实现细节**:

- HashMap key 使用 `(message, severity_number, code)` 三元组
- code 为 `Option` 时,`None` 视为同组
- 复杂度 O(n),n = 诊断条数
- 分组后每个 group 的 ranges 按位置排序(line→col),保持输出可读

### Strategy 2: 字段裁剪 (Field Pruning)

**默认保留字段**:
- `message`: 诊断消息
- `severity`: 严重级别
- `range`: 位置范围(压缩为 `[line, char, line, char]`- `code`: 诊断代码(可选,保留时有助于 AI 理解错误类型)

**默认移除字段**:
- `data`: 额外数据(极少使用)
- `codeDescription`: 代码描述(极少使用)
- `relatedInformation`: 相关信息(AI 很少需要)
- `tags`: 标签(压缩后编码到单字符)
- `source`: 来源(固定值,对 AI 无意义)

### Strategy 3: 枚举缩减 (Enum Compression)

**Severity 编码**:

| 原始值 | 压缩值 | 说明 |
|--------|--------|------|
| `1` (Error) | `'E'` | 错误 |
| `2` (Warning) | `'W'` | 警告 |
| `3` (Information) | `'I'` | 信息 |
| `4` (Hint) | `'H'` | 提示 |

**Tags 编码** (如有):
- `Unnecessary`: `'U'`
- `Deprecated`: `'D'`

### Strategy 4: Range Delta 编码

**原始格式** — 每个 range 需要 6 个字段名 + 4 个数字:
```json
{"start": {"line": 10, "character": 5}, "end": {"line": 10, "character": 15}}
```

**压缩格式** — 4 个数字数组,无字段名:
```
[10, 5, 10, 15]
```

**多个 Range**: 第一个 range 存绝对值,后续 range 存相对于前一个的差值(delta 编码)。这样当多个错误在同一行或连续几行时,增量值很小甚至为零。

#### Delta 编码算法

```mermaid
flowchart LR
    subgraph 输入["输入: 3 个 LSP Range"]
        R1["Range{start:{10,5}, end:{10,15}}"]
        R2["Range{start:{11,5}, end:{11,15}}"]
        R3["Range{start:{12,5}, end:{12,15}}"]
    end

    subgraph 绝对值["第一步: 转 [L,C,L,C]"]
        A1["[10, 5, 10, 15]"]
        A2["[11, 5, 11, 15]"]
        A3["[12, 5, 12, 15]"]
    end

    subgraph 差分["第二步: 计算 Delta"]
        D1["第一个: 保持绝对 → [10, 5, 10, 15]"]
        D2["第二个: [11-10, 5-5, 11-10, 15-15] = [1, 0, 1, 0]"]
        D3["第三个: [12-11, 5-5, 12-11, 15-15] = [1, 0, 1, 0]"]
    end

    subgraph 输出["输出: delta 编码数组"]
        O1["[10, 5, 10, 15] ← 绝对值"]
        O2["[1, 0, 1, 0] ← delta"]
        O3["[1, 0, 1, 0] ← delta"]
    end

    R1 --> A1
    R2 --> A2
    R3 --> A3
    A1 --> D1 --> O1
    A2 --> D2 --> O2
    A3 --> D3 --> O3

    classDef input fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
    classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
    classDef output fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff

    class R1,R2,R3 input
    class A1,A2,A3,D1,D2,D3 step
    class O1,O2,O3 output
```

**解码时还原**: 累加 delta 值恢复绝对值。

```json
// 存储:
{
  "r": [
    [10, 5, 10, 15],  // 第一个: 绝对值
    [1, 0, 1, 0],      // 第二个: line+1, col 不变
    [1, 0, 1, 0]       // 第三个: line+1, col 不变
  ]
}

// 还原: 累加 delta
// range[1] = [10+1, 5+0, 10+1, 15+0] = [11, 5, 11, 15]
// range[2] = [11+1, 5+0, 11+1, 15+0] = [12, 5, 12, 15]
```

**何时不启用 delta**: 当只有 1 个 range 时,直接存绝对值 `[line, col, line, col]`,不做 delta。

---

## 紧凑格式 Schema

### 顶层结构

```typescript
interface CompactDiagnosticsParams {
  version: 1;           // 格式版本
  uri: string;          // 文档 URI
  diagnostics: CompactDiagnostic[];
}
```

### 单个诊断

```typescript
interface CompactDiagnostic {
  m: string;                    // message
  s: 'E' | 'W' | 'I' | 'H';    // severity (单字符)
  r: number[][];               // ranges (Array of [line_start, char_start, line_end, char_end], delta 编码)

  // 可选字段
  c?: string | number;          // code (保留时有助于 AI 按错误类型分组)
  t?: string;                   // tags (逗号分隔单字符: "U" / "D" / "U,D")
  n?: number;                   // count (dedup 合并的诊断条数, 默认 1)
}
```

### Range 格式

```typescript
// 绝对 range: [line_start, char_start, line_end, char_end]
// delta  range: [Δline_start, Δchar_start, Δline_end, Δchar_end] (相对于上一个)
type Range = [number, number, number, number];
type Ranges = Range[];  // 第一个是绝对,后续是 delta
```

---

## 完整示例

### 原始 LSP Diagnostics

```json
{
  "uri": "file:///path/to/file.rs",
  "diagnostics": [
    {
      "range": {
        "start": {"line": 10, "character": 5},
        "end": {"line": 10, "character": 15}
      },
      "severity": 2,
      "message": "unused variable: `x`",
      "code": "unused_variables",
      "source": "rust-analyzer",
      "tags": [1]
    },
    {
      "range": {
        "start": {"line": 11, "character": 5},
        "end": {"line": 11, "character": 15}
      },
      "severity": 2,
      "message": "unused variable: `x`",
      "code": "unused_variables",
      "source": "rust-analyzer",
      "tags": [1]
    },
    {
      "range": {
        "start": {"line": 20, "character": 0},
        "end": {"line": 20, "character": 10}
      },
      "severity": 1,
      "message": "cannot find value `y` in this scope",
      "code": "E0425",
      "source": "rust-analyzer"
    }
  ]
}
```

### 紧凑格式

```json
{
  "version": 1,
  "uri": "file:///path/to/file.rs",
  "diagnostics": [
    {
      "m": "unused variable: `x`",
      "s": "W",
      "r": [[10, 5, 10, 15], [1, 0, 1, 0]],
      "c": "unused_variables",
      "t": "U",
      "n": 2
    },
    {
      "m": "cannot find value `y` in this scope",
      "s": "E",
      "r": [[20, 0, 20, 10]],
      "c": "E0425"
    }
  ]
}
```

### Token 对比

| 格式 | 估算 Token (GPT-4) | 节省 |
|------|-------------------|------|
| 原始 JSON | ~350 | - |
| 紧凑格式 | ~180 | **48%** |

> 注: 上述示例只有 3 条诊断。在真实场景中(如一个 TypeScript 文件有 50+ 个 lint 错误),去重合并带来的收益更大。

---

## 压缩管道

```mermaid
flowchart LR
    subgraph RAW["Raw Diagnostics[]"]
        I1["{range, severity, message, code, source, tags}"]
        I2["{range, severity, message, code, source, tags}"]
        I3["{range, severity, message, code, source}"]
    end

    subgraph S1["Step 1: Group & Dedup"]
        G1["Hash by (message, severity, code)"]
        G2["Merge ranges"]
        G3["Count n"]
    end

    subgraph S2["Step 2: Prune Fields"]
        P1["Drop: source, data, codeDescription, relatedInformation"]
        P2["Keep: message, severity, range, code"]
    end

    subgraph S3["Step 3: Encode"]
        E1["severity: 1→E 2→W 3→I 4→H"]
        E2["tags: [1]→'U'"]
        E3["keys: m, s, r, c, n"]
    end

    subgraph S4["Step 4: Delta Ranges"]
        D1["First: absolute"]
        D2["Rest: delta"]
    end

    subgraph COMPACT["CompactDiagnostics{v, uri, diags[]}"]
        O1["{m: '...', s: 'W', r: [[10,5,10,15],[1,0,1,0]], c: 'code', n: 2}"]
        O2["{m: '...', s: 'E', r: [[20,0,20,10]], c: 'E0425'}"]
    end

    I1 & I2 & I3 --> G1 --> G2 --> G3
    G3 --> P1 --> P2
    P2 --> E1 --> E2 --> E3
    E3 --> D1 --> D2
    D2 --> O1
    D2 --> O2

    classDef raw fill:#7ED321,stroke:#5BA01A,stroke-width:2px,color:#fff
    classDef step fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff
    classDef compact fill:#F5A623,stroke:#D4880F,stroke-width:2px,color:#fff

    class I1,I2,I3 raw
    class G1,G2,G3,P1,P2,E1,E2,E3,D1,D2 step
    class O1,O2 compact
```

---

## 版本控制

### 格式版本字段

所有紧凑格式消息必须包含 `version` 字段:

```json
{
  "version": 1,
  "uri": "file:///path/to/file.rs",
  "diagnostics": [...]
}
```

### 版本兼容性

| Agent 端版本 | 服务端版本 | 兼容性 |
|-------------|-----------|--------|
| v1 | v1 | ✅ 完全兼容 |
| v2 | v1 | ⚠️ 可降级 |
| v1 | v2 | ❌ 需升级 |

### 升级策略

1. 增加版本号
2. Agent 端和 Proxy 端同时更新
3. 提供兼容层支持旧版本(临时)

---

## 输出格式进化

当前 Compact JSON 作为第一个输出格式,后续引入 [**TOON(Token-Oriented Object Notation)**](005-toon-format.md) 作为 LLM 优化的第二输出格式。

| 格式 | 阶段 | 说明 |
|------|------|------|
| Compact JSON | 当前 (v0.1–v0.5) | JSON 格式,字段缩短 + 去重,最小 token |
| TOON | v0.7+ | 表格格式+自解释字段名,LLM 直接消费 |

**选择指南**: 如果需要最小 token → 用 Compact JSON(缩写字段名)。
如果希望 LLM 直接理解 → 用 TOON(完整字段名,无需预解压)。
详见 [005-toon-format.md](005-toon-format.md)。

---

## 配置选项

```rust
/// 压缩策略的细粒度控制。
pub struct CompressionConfig {
    /// 是否启用去重合并(#1 收益策略)
    pub enable_dedup: bool,
    /// 是否启用字段裁剪
    pub enable_field_pruning: bool,
    /// 是否启用枚举缩减
    pub enable_enum_compression: bool,
    /// 是否启用 Range delta 编码
    pub enable_range_delta: bool,
    /// 保留字段列表(当 enable_field_pruning=true 时)
    pub keep_fields: Vec<String>,
}

impl Default for CompressionConfig {
    fn default() -> Self {
        Self {
            enable_dedup: true,
            enable_field_pruning: true,
            enable_enum_compression: true,
            enable_range_delta: true,
            keep_fields: vec!["message".into(), "severity".into(), "range".into(), "code".into()],
        }
    }
}
```

---

## 参考文档

- [LSP Diagnostic 类型]https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic
- [ROADMAP.md]https://github.com/straydragon/lspz/blob/main/ROADMAP.md - 项目路线图
- [specs/001-tri-modal-architecture.md]001-tri-modal-architecture.md - 三模态架构
- [specs/003-lsp-compatibility.md]003-lsp-compatibility.md - LSP 兼容性
- [specs/005-toon-format.md]005-toon-format.md - TOON 行协议格式