Skip to content

Commit 7d9d5b2

Browse files
author
SqlRush
committed
Persist LSP diagnostics notifications
1 parent e840b53 commit 7d9d5b2

4 files changed

Lines changed: 114 additions & 6 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ M7 补充:terminal input parser 和 configurable keybinding name parser 现在
14561456
- Chrome/computer-use/voice/native integrations。
14571457
- enterprise/gated/platform-specific behavior。
14581458

1459-
当前状态:已新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload 并按文件替换 snapshot;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,并提供 command/display/alias 安全解析;未启用时仍不注册或泄露 gated 工具 schema。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
1459+
当前状态:已新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot,并按 LSP 空 diagnostics 语义清空旧文件诊断;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,并提供 command/display/alias 安全解析;未启用时仍不注册或泄露 gated 工具 schema。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
14601460

14611461
## Recommended Next Steps
14621462

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ test/parity/ # golden tests against TS/official behavior
745745
- 每个 gated feature 独立开关测试。
746746
- 不启用 feature 时二进制行为和可见 schema 不泄露 gated 工具/命令。
747747

748-
- 本轮补充:新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload 并按文件替换 snapshot;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,并提供 command/display/alias 安全解析;未启用时仍不注册或泄露 gated 工具 schema。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
748+
- 本轮补充:新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot,并按 LSP 空 diagnostics 语义清空旧文件诊断;`advanced.bridge=true` 时会写出 session-scoped bridge manifest,列出 bridge-safe slash/local command 元数据,并提供 command/display/alias 安全解析;未启用时仍不注册或泄露 gated 工具 schema。实际 repl bridge server、session websocket/direct connect、LSP server manager、diagnostic producer、telemetry exporter/tracing、Chrome/voice/computer-use/native runtime 仍未完成。
749749

750750
### M12: Parity hardening
751751

internal/lsp/diagnostics.go

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ type PublishDiagnosticRecord struct {
4949
Message string `json:"message"`
5050
}
5151

52+
type DiagnosticsUpdate struct {
53+
FilePath string
54+
Diagnostics []Diagnostic
55+
}
56+
5257
type Filter struct {
5358
FilePath string
5459
Severity string
@@ -95,22 +100,49 @@ func LoadSnapshot(path string) ([]Diagnostic, error) {
95100
return NormalizeDiagnostics(diagnostics), nil
96101
}
97102

103+
func ApplyPublishDiagnosticsSnapshot(path string, data []byte) ([]Diagnostic, error) {
104+
if path == "" {
105+
return nil, os.ErrInvalid
106+
}
107+
update, err := DiagnosticsUpdateFromPublishDiagnostics(data)
108+
if err != nil {
109+
return nil, err
110+
}
111+
existing, err := LoadSnapshot(path)
112+
if err != nil {
113+
return nil, err
114+
}
115+
updated := ApplyDiagnosticsForFile(existing, update.FilePath, update.Diagnostics)
116+
if err := WriteSnapshot(path, updated); err != nil {
117+
return nil, err
118+
}
119+
return updated, nil
120+
}
121+
98122
func DiagnosticsFromPublishDiagnostics(data []byte) ([]Diagnostic, error) {
123+
update, err := DiagnosticsUpdateFromPublishDiagnostics(data)
124+
if err != nil {
125+
return nil, err
126+
}
127+
return update.Diagnostics, nil
128+
}
129+
130+
func DiagnosticsUpdateFromPublishDiagnostics(data []byte) (DiagnosticsUpdate, error) {
99131
var params PublishDiagnosticsParams
100132
if err := json.Unmarshal(data, &params); err != nil {
101-
return nil, err
133+
return DiagnosticsUpdate{}, err
102134
}
103135
if strings.TrimSpace(params.URI) == "" {
104136
var wrapper struct {
105137
Params PublishDiagnosticsParams `json:"params"`
106138
}
107139
if err := json.Unmarshal(data, &wrapper); err != nil {
108-
return nil, err
140+
return DiagnosticsUpdate{}, err
109141
}
110142
params = wrapper.Params
111143
}
112144
if strings.TrimSpace(params.URI) == "" {
113-
return nil, fmt.Errorf("publishDiagnostics uri is required")
145+
return DiagnosticsUpdate{}, fmt.Errorf("publishDiagnostics uri is required")
114146
}
115147
filePath := URIToPath(params.URI)
116148
out := make([]Diagnostic, 0, len(params.Diagnostics))
@@ -124,7 +156,10 @@ func DiagnosticsFromPublishDiagnostics(data []byte) ([]Diagnostic, error) {
124156
Message: diagnostic.Message,
125157
})
126158
}
127-
return NormalizeDiagnostics(out), nil
159+
return DiagnosticsUpdate{
160+
FilePath: normalizePath(filePath),
161+
Diagnostics: NormalizeDiagnostics(out),
162+
}, nil
128163
}
129164

130165
func ApplyDiagnosticsUpdate(existing []Diagnostic, update []Diagnostic) []Diagnostic {
@@ -147,6 +182,25 @@ func ApplyDiagnosticsUpdate(existing []Diagnostic, update []Diagnostic) []Diagno
147182
return NormalizeDiagnostics(out)
148183
}
149184

185+
func ApplyDiagnosticsForFile(existing []Diagnostic, filePath string, diagnostics []Diagnostic) []Diagnostic {
186+
filePath = normalizePath(filePath)
187+
if filePath == "" {
188+
return NormalizeDiagnostics(existing)
189+
}
190+
out := make([]Diagnostic, 0, len(existing)+len(diagnostics))
191+
for _, diagnostic := range NormalizeDiagnostics(existing) {
192+
if diagnostic.FilePath == filePath {
193+
continue
194+
}
195+
out = append(out, diagnostic)
196+
}
197+
for _, diagnostic := range diagnostics {
198+
diagnostic.FilePath = filePath
199+
out = append(out, diagnostic)
200+
}
201+
return NormalizeDiagnostics(out)
202+
}
203+
150204
func URIToPath(raw string) string {
151205
raw = strings.TrimSpace(raw)
152206
parsed, err := url.Parse(raw)

internal/lsp/diagnostics_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ func TestDiagnosticsFromPublishDiagnosticsWrapper(t *testing.T) {
111111
}
112112
}
113113

114+
func TestDiagnosticsUpdateFromPublishDiagnosticsPreservesEmptyUpdates(t *testing.T) {
115+
update, err := DiagnosticsUpdateFromPublishDiagnostics([]byte(`{
116+
"uri": "file:///work/main.go",
117+
"diagnostics": []
118+
}`))
119+
if err != nil {
120+
t.Fatal(err)
121+
}
122+
if update.FilePath != "/work/main.go" || len(update.Diagnostics) != 0 {
123+
t.Fatalf("update = %#v", update)
124+
}
125+
}
126+
114127
func TestDiagnosticsFromPublishDiagnosticsRequiresURI(t *testing.T) {
115128
_, err := DiagnosticsFromPublishDiagnostics([]byte(`{"diagnostics":[]}`))
116129
if err == nil || !strings.Contains(err.Error(), "uri is required") {
@@ -139,6 +152,47 @@ func TestApplyDiagnosticsUpdateReplacesUpdatedFiles(t *testing.T) {
139152
}
140153
}
141154

155+
func TestApplyDiagnosticsForFileClearsUpdatedFile(t *testing.T) {
156+
existing := []Diagnostic{
157+
{FilePath: "a.go", Severity: "error", Message: "old a"},
158+
{FilePath: "b.go", Severity: "warning", Message: "old b"},
159+
}
160+
got := ApplyDiagnosticsForFile(existing, "./a.go", nil)
161+
if len(got) != 1 || got[0].FilePath != "b.go" || got[0].Message != "old b" {
162+
t.Fatalf("updated diagnostics = %#v", got)
163+
}
164+
}
165+
166+
func TestApplyPublishDiagnosticsSnapshotWritesReplacement(t *testing.T) {
167+
path := filepath.Join(t.TempDir(), diagnosticsFileName)
168+
if err := WriteSnapshot(path, []Diagnostic{
169+
{FilePath: "/a.go", Severity: "error", Message: "old a"},
170+
{FilePath: "b.go", Severity: "warning", Message: "old b"},
171+
}); err != nil {
172+
t.Fatal(err)
173+
}
174+
updated, err := ApplyPublishDiagnosticsSnapshot(path, []byte(`{
175+
"uri": "file:///a.go",
176+
"diagnostics": [{"severity": 1, "message": "new a"}]
177+
}`))
178+
if err != nil {
179+
t.Fatal(err)
180+
}
181+
if len(updated) != 2 || updated[0].FilePath != "/a.go" || updated[0].Message != "new a" || updated[1].FilePath != "b.go" {
182+
t.Fatalf("updated diagnostics = %#v", updated)
183+
}
184+
updated, err = ApplyPublishDiagnosticsSnapshot(path, []byte(`{
185+
"uri": "file:///a.go",
186+
"diagnostics": []
187+
}`))
188+
if err != nil {
189+
t.Fatal(err)
190+
}
191+
if len(updated) != 1 || updated[0].FilePath != "b.go" {
192+
t.Fatalf("cleared diagnostics = %#v", updated)
193+
}
194+
}
195+
142196
func TestApplyDiagnosticsUpdateIgnoresEmptyUpdate(t *testing.T) {
143197
existing := []Diagnostic{{FilePath: "a.go", Severity: "error", Message: "old a"}}
144198
got := ApplyDiagnosticsUpdate(existing, nil)

0 commit comments

Comments
 (0)