Skip to content

Commit d4312ef

Browse files
SimplyLizclaude
andcommitted
release: v9.1.0 — LIP v2.1 utilisation, index status UX, bug-pattern fix
Wire up three LIP v2.1 RPCs (stream_context, query_expansion, explain_match), probe LIP index status on handshake and surface a clear warning in `ckb review` when the daemon has no content. Fix Lock() false positive in bug-patterns checker and eliminate err shadowing in subscribe.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3e5a064 commit d4312ef

File tree

9 files changed

+131
-76
lines changed

9 files changed

+131
-76
lines changed

CHANGELOG.md

Lines changed: 43 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,79 +4,58 @@ All notable changes to CKB will be documented in this file.
44

55
## [Unreleased]
66

7+
## [9.1.0] - 2026-04-16
8+
79
### Added
810

9-
- **`explainFile` surfaces semantically-related symbols** via LIP v2.1's
10-
`stream_context` RPC (`internal/query/lip_stream_context.go`). The daemon
11-
ranks symbols across the whole file within a 2048-token budget; CKB
12-
returns the top 10 in the new `facts.related` field with per-symbol
13-
relevance and token cost. Gated on the handshake's `supported_messages`
14-
— older daemons fall through and the field is absent. New streaming
15-
transport `internal/lip/stream_context.go` reads the daemon's N
16-
`symbol_info` frames plus the `end_stream` terminator; previous LIP
17-
client was unary-only.
18-
- **`searchSymbols` expands short queries** via LIP's `query_expansion`
19-
RPC (`internal/query/query_expansion.go`). Queries of ≤ 2 tokens get up
20-
to 5 related terms appended before hitting FTS5, recovering recall on
21-
vocabulary-mismatch misses ("auth" → "authenticate authorization
22-
principal…"). Gated on the handshake and on the same mixed-models flag
23-
that protects the rerank path. Longer queries are passed through
24-
unchanged — the expansion is a rescue, not a rewrite.
25-
- **Semantic hits carry evidence chunks** when LIP v2.0+'s `explain_match`
26-
is advertised (`SemanticSearchWithLIPExplained` in
27-
`internal/query/lip_ranker.go`). Each hit returned by the semantic
28-
fallback path now includes up to two ranked chunks with line ranges,
29-
text, and per-chunk scores — the caller can cite specific lines instead
30-
of a bare file URL. Capped at the top-5 hits to bound round-trip cost.
11+
- **LIP v2.1 utilisation** — three high-ROI LIP RPCs wired into the query
12+
engine, gated on the handshake's `supported_messages`:
13+
- `stream_context` (v2.1) → `explainFile` attaches up to 10
14+
semantically-related symbols (2048-token budget) in `facts.related`.
15+
New streaming transport reads N `symbol_info` frames + `end_stream`.
16+
- `query_expansion` (v1.6) → `searchSymbols` expands ≤ 2-token queries
17+
with up to 5 related terms before FTS5, recovering vocabulary-mismatch
18+
recall without touching precision on compound queries.
19+
- `explain_match` (v2.0) → semantic search hits carry up to two ranked
20+
evidence chunks with line ranges, text, and per-chunk scores (top-5
21+
hits, bounded round-trip cost).
3122
- **`lip.Handshake` runs on engine startup** and the daemon's
3223
`supported_messages` list is stashed for feature gating
33-
(`Engine.lipSupports`). The daemon version and supported-count are
34-
logged on connect.
35-
24+
(`Engine.lipSupports`). Daemon version and supported-count logged.
25+
- **LIP index status probing**`probeHandshake` now follows up with
26+
`IndexStatus` and caches the result. New `Engine.LIPStatus()` returns
27+
`{Reachable, IndexedFiles}` so consumers can distinguish "daemon down"
28+
from "daemon up, nothing indexed."
29+
- **`ckb review` warns when LIP index is empty** — stderr advisory with
30+
`lip index <repo>` command when daemon is reachable but has no content.
31+
Suppressed in `--ci` to keep CI logs clean.
32+
- `NoAutoFetch` option on `SummarizePROptions` and `SummarizeDiffOptions`
33+
for parity with `ReviewPROptions`.
34+
- Troubleshooting section in `docs/plans/review-cicd.md` covering shallow
35+
CI clones, auth-failure remediation, air-gapped pipelines, and depth-0
36+
checkout alternatives.
37+
- Auth-error detection on auto-fetch with clear remediation guidance.
38+
- `ckb review --no-auto-fetch` flag for air-gapped pipelines.
39+
- Test coverage for `GitAdapter.EnsureRef` — happy path, missing-ref
40+
auto-fetch, unreachable origin, and empty-input guard.
3641
### Changed
3742

38-
- **`lipFileURI` path normalisation** — the helper that builds
39-
`file://`-URIs for LIP requests used to naive-`filepath.Join` whatever
40-
`Location.FileId` a backend supplied. Now handles absolute paths and
41-
already-prefixed `file://` URIs without producing malformed results
42-
like `file:///repo//abs/path`. Backends today return relative paths, so
43-
this is a hardening fix for contracts that are nominally open.
44-
45-
### Changed
43+
- **LIP health: push-driven, not polled** — Engine opens a long-lived
44+
connection to the daemon at startup (`internal/lip/subscribe.go`) with
45+
`index_changed` frames and per-ping `index_status` snapshots instead of
46+
60 s TTL polling. Worst-case staleness drops from 60 s to ~3 s.
47+
- **`lipFileURI` path normalisation** — handles absolute paths and
48+
already-prefixed `file://` URIs without producing malformed results.
4649

47-
- **LIP health: push-driven, not polled** — the Engine now opens a long-lived
48-
connection to the LIP daemon at startup (`internal/lip/subscribe.go`) and
49-
receives `index_changed` frames plus per-ping `index_status` snapshots
50-
instead of issuing a fresh `IndexStatus` RPC on a 60 s TTL. Worst-case
51-
staleness for the mixed-models gate drops from 60 s to ~3 s, the hot query
52-
path is lock-free (no RPC, no dial, no TTL check), and the subscriber
53-
reconnects with exponential backoff when the daemon restarts. The old
54-
`lipHealthTTL` constant is gone; callers read the cached flag directly.
50+
### Fixed
5551

56-
### Added
52+
- **Bug-pattern false positive on `sync.Mutex.Lock()`** — removed `"Lock"`
53+
from `LikelyReturnsError` heuristic patterns; `sync.Mutex.Lock` returns
54+
nothing and dominated real-world matches with false positives.
55+
- **`err` shadowing in `subscribe.go`** — four shadow sites eliminated by
56+
reusing outer `err` or renaming to `pingErr`/`readErr` where scope
57+
isolation requires it.
5758

58-
- `NoAutoFetch` option on `SummarizePROptions` and `SummarizeDiffOptions`
59-
for parity with `ReviewPROptions`. Previously the air-gapped opt-out
60-
applied only to `ckb review`; MCP/HTTP callers of `summarizePr` and
61-
`summarizeDiff` had no way to disable auto-fetch.
62-
- Troubleshooting section in `docs/plans/review-cicd.md` covering shallow
63-
CI clones, auth-failure remediation (`persistCredentials`), air-gapped
64-
pipelines, and depth-0 checkout alternatives.
65-
- Auth-error detection on auto-fetch: when `git fetch` fails with
66-
`Authentication failed`, `could not read Username`, `terminal prompts
67-
disabled`, `401 Unauthorized`, `403 Forbidden`, `Permission denied
68-
(publickey)`, or `repository ... not found`, the review error now names
69-
the root cause (stripped CI credentials) and points at
70-
`persistCredentials` / `--no-auto-fetch` as remediations, instead of
71-
dumping raw git stderr.
72-
- `ckb review --no-auto-fetch` flag disables the automatic fetch of a
73-
missing base ref introduced in 9.0.1. Also exposed on `ReviewPROptions`
74-
as `noAutoFetch` for MCP / HTTP callers. Useful in air-gapped pipelines
75-
where network activity outside the checkout step is forbidden; review
76-
fails with a clear "ref not present locally" error instead.
77-
- Test coverage for `GitAdapter.EnsureRef` — happy path, missing-ref
78-
auto-fetch, unreachable origin, and empty-input guard — using isolated
79-
bare+clone repo pairs in temp dirs.
8059
- **LIP rerank: coherence gate + position-weighted seeding** (#209) — the
8160
Fast-tier semantic rerank (`internal/query/lip_ranker.go`) used to average the
8261
top-5 seed embeddings with uniform weight and always apply the result. When

cmd/ckb/review.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ func runReview(cmd *cobra.Command, args []string) {
233233
NoAutoFetch: reviewNoAutoFetch,
234234
}
235235

236+
warnIfLIPIndexEmpty(engine, repoRoot)
237+
236238
response, err := engine.ReviewPR(ctx, opts)
237239
if err != nil {
238240
fmt.Fprintf(os.Stderr, "Error running review: %v\n", err)
@@ -314,6 +316,27 @@ func runReview(cmd *cobra.Command, args []string) {
314316
}
315317
}
316318

319+
// warnIfLIPIndexEmpty prints a stderr advisory when the LIP daemon is
320+
// reachable but has no content indexed. Semantic enrichment (stream_context,
321+
// query_expansion, explain_match) silently returns empty in that case, which
322+
// is indistinguishable from "feature disabled" from the user's side — so we
323+
// tell them and show the exact command to build the index.
324+
//
325+
// The warning goes to stderr so it doesn't pollute JSON/SARIF stdout that
326+
// CI pipelines parse. Suppressed in --ci mode to keep CI logs tight.
327+
func warnIfLIPIndexEmpty(engine *query.Engine, repoRoot string) {
328+
if reviewCI {
329+
return
330+
}
331+
s := engine.LIPStatus()
332+
if !s.Reachable || s.IndexedFiles > 0 {
333+
return
334+
}
335+
fmt.Fprintln(os.Stderr, "⚠ LIP daemon reachable but has no index for this workspace.")
336+
fmt.Fprintln(os.Stderr, " Semantic enrichment (related symbols, query expansion, explain-match) will be skipped.")
337+
fmt.Fprintf(os.Stderr, " Run: lip index %s\n\n", repoRoot)
338+
}
339+
317340
// --- Output Formatters ---
318341

319342
func formatReviewHuman(resp *query.ReviewPRResponse) string {

internal/backends/scip/symbols.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,14 +513,17 @@ func LikelyReturnsError(symbolName string) bool {
513513
return false
514514
}
515515

516-
// Common Go stdlib/convention patterns for error-returning functions
516+
// Common Go stdlib/convention patterns for error-returning functions.
517+
// "Lock" is deliberately absent: sync.{Mutex,RWMutex}.Lock dominates real
518+
// code and returns nothing, so the rule produces far more false positives
519+
// than the rare file-lock true positive it would catch.
517520
errorPatterns := []string{
518521
"Open", "Read", "Write", "Close", "Create",
519522
"Dial", "Listen", "Accept", "Connect",
520523
"Parse", "Unmarshal", "Marshal", "Decode", "Encode",
521524
"Execute", "Exec", "Query",
522525
"Send", "Recv", "Flush",
523-
"Lock", "Acquire",
526+
"Acquire",
524527
"Start", "Stop", "Init", "Setup",
525528
}
526529

internal/lip/subscribe.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func runSession(ctx context.Context, h SubscribeHandler) error {
110110
defer writeMu.Unlock()
111111
return writeFrame(conn, map[string]any{"type": "index_status"})
112112
}
113-
if err := ping(); err != nil {
113+
if err = ping(); err != nil {
114114
return err
115115
}
116116

@@ -123,7 +123,7 @@ func runSession(ctx context.Context, h SubscribeHandler) error {
123123
case <-sessCtx.Done():
124124
return
125125
case <-t.C:
126-
if err := ping(); err != nil {
126+
if pingErr := ping(); pingErr != nil {
127127
cancel()
128128
return
129129
}
@@ -136,11 +136,11 @@ func runSession(ctx context.Context, h SubscribeHandler) error {
136136
if sessCtx.Err() != nil {
137137
break
138138
}
139-
frame, err := readFrame(conn)
140-
if err != nil {
139+
frame, readErr := readFrame(conn)
140+
if readErr != nil {
141141
cancel()
142142
wg.Wait()
143-
return err
143+
return readErr
144144
}
145145
dispatchFrame(frame, h)
146146
}
@@ -206,7 +206,7 @@ func writeFrame(conn net.Conn, payload any) error {
206206
lenBuf := make([]byte, 4)
207207
binary.BigEndian.PutUint32(lenBuf, uint32(len(b)))
208208
_ = conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
209-
if _, err := conn.Write(append(lenBuf, b...)); err != nil {
209+
if _, err = conn.Write(append(lenBuf, b...)); err != nil {
210210
return err
211211
}
212212
return nil

internal/query/engine.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ type Engine struct {
8080
lipHealthCheckedAt time.Time
8181
lipSupported map[string]struct{}
8282
lipSubCancel context.CancelFunc
83+
// lipIndexProbed is true once probeHandshake has attempted an IndexStatus
84+
// call. lipIndexedFiles==0 with lipIndexProbed==true means the daemon is
85+
// reachable but has nothing indexed — surface this to the user so semantic
86+
// enrichment silence doesn't look like a bug.
87+
lipIndexProbed bool
88+
lipIndexedFiles int
8389

8490
// Cache stats
8591
cacheStatsMu sync.RWMutex

internal/query/lip_health.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,58 @@ func (e *Engine) probeHandshake() {
9696
for _, m := range info.SupportedMessages {
9797
supported[m] = struct{}{}
9898
}
99+
100+
// Follow up with a cheap IndexStatus probe so callers can distinguish
101+
// "daemon down" from "daemon up but has no content for this workspace".
102+
// Failures here are non-fatal — we just leave lipIndexProbed=false and
103+
// consumers treat that as "unknown, don't warn".
104+
status, _ := lip.IndexStatus()
105+
99106
e.lipHealthMu.Lock()
100107
e.lipSupported = supported
108+
if status != nil {
109+
e.lipIndexProbed = true
110+
e.lipIndexedFiles = status.IndexedFiles
111+
}
101112
e.lipHealthMu.Unlock()
113+
102114
if e.logger != nil {
115+
files := -1
116+
if status != nil {
117+
files = status.IndexedFiles
118+
}
103119
e.logger.Info("LIP handshake",
104120
"daemon_version", info.DaemonVersion,
105121
"protocol_version", info.ProtocolVersion,
106122
"supported_count", len(info.SupportedMessages),
123+
"indexed_files", files,
107124
)
108125
}
109126
}
127+
128+
// LIPIndexStatus is a UX-facing snapshot of LIP daemon reachability and
129+
// workspace coverage. Consumers (e.g. `ckb review`, `ckb status`) call this
130+
// after engine startup to decide whether to show a "no LIP index" warning.
131+
//
132+
// - Reachable=false: daemon not running or handshake didn't complete.
133+
// Silence is expected; no warning.
134+
// - Reachable=true, IndexedFiles=0: daemon up, nothing indexed. Semantic
135+
// enrichment will silently return empty — warn the user and offer the
136+
// `lip index` command.
137+
// - Reachable=true, IndexedFiles>0: happy path.
138+
type LIPIndexStatus struct {
139+
Reachable bool
140+
IndexedFiles int
141+
DaemonVersion string
142+
}
143+
144+
// LIPStatus returns the cached handshake + IndexStatus snapshot captured at
145+
// engine startup. Cheap — no RPC on the hot path.
146+
func (e *Engine) LIPStatus() LIPIndexStatus {
147+
e.lipHealthMu.RLock()
148+
defer e.lipHealthMu.RUnlock()
149+
return LIPIndexStatus{
150+
Reachable: e.lipIndexProbed,
151+
IndexedFiles: e.lipIndexedFiles,
152+
}
153+
}

internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package version
66
// go build -ldflags "-X github.com/SimplyLiz/CodeMCP/internal/version.Version=1.0.0 -X github.com/SimplyLiz/CodeMCP/internal/version.Commit=abc123"
77
var (
88
// Version is the semantic version of CKB
9-
Version = "9.0.1"
9+
Version = "9.1.0"
1010

1111
// Commit is the git commit hash (set at build time)
1212
Commit = "unknown"

npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@tastehub/ckb",
33
"mcpName": "io.github.SimplyLiz/ckb",
4-
"version": "9.0.1",
4+
"version": "9.1.0",
55
"description": "Code intelligence for AI assistants (MCP), CLI, and HTTP API - symbol navigation, impact analysis, architecture",
66
"keywords": [
77
"mcp",

testdata/review/sarif.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@
268268
}
269269
}
270270
],
271-
"semanticVersion": "9.0.1",
272-
"version": "9.0.1"
271+
"semanticVersion": "9.1.0",
272+
"version": "9.1.0"
273273
}
274274
}
275275
}

0 commit comments

Comments
 (0)