Skip to content

Commit 32dc5c2

Browse files
authored
Merge pull request #213 from SimplyLiz/develop
release: v9.1.0
2 parents 21d5da6 + d4312ef commit 32dc5c2

25 files changed

+1428
-98
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ bin/
88
/ckb
99
/ckb-test
1010
/ckb-bench
11+
/ckb_fresh
1112
coverage.out
1213
*_test
14+
*.test
1315
*.scip
1416

1517
# Registry / credential tokens

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,82 @@
22

33
All notable changes to CKB will be documented in this file.
44

5+
## [Unreleased]
6+
7+
## [9.1.0] - 2026-04-16
8+
9+
### Added
10+
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).
22+
- **`lip.Handshake` runs on engine startup** and the daemon's
23+
`supported_messages` list is stashed for feature gating
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.
41+
### Changed
42+
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.
49+
50+
### Fixed
51+
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.
58+
59+
- **LIP rerank: coherence gate + position-weighted seeding** (#209) — the
60+
Fast-tier semantic rerank (`internal/query/lip_ranker.go`) used to average the
61+
top-5 seed embeddings with uniform weight and always apply the result. When
62+
the top-5 pointed in different directions the centroid collapsed toward zero
63+
and amplified noise; when the top seed was strong the blend still diluted it.
64+
Seeds are now L2-normalised and position-weighted (`1/(rank+1)`), the
65+
resulting centroid norm is read as a coherence score in `[0, 1]`, and the
66+
rerank falls back to pure lexical order when coherence is below
67+
`MinCoherence` (default `0.35`). Blend weights, seed count, and threshold are
68+
surfaced as `RerankConfig` so future tuning does not need to touch call
69+
sites. Injected `embedBatchFn` makes the ranker unit-testable without a
70+
running daemon.
71+
- **LIP rerank: gate on `!MixedModels`** (#208) — when the LIP index contains
72+
vectors from more than one embedding model (e.g. partial re-index during a
73+
model upgrade), cosine similarity across those vectors is mathematically
74+
meaningless. `RerankWithLIP` and `SemanticSearchWithLIP` now consult a
75+
cached `Engine.lipSemanticAvailable()` check (60 s TTL, single `IndexStatus`
76+
RPC) and fall back to lexical ranking when the daemon is down or reports
77+
`mixed_models`. A new `lip_mixed_models` degradation warning (70% capability)
78+
surfaces in response metadata so users learn *why* results look weaker
79+
instead of silently ranking on garbage.
80+
581
## [9.0.1] - 2026-04-15
682

783
### Fixed

cmd/ckb-bench/version_test.go

Lines changed: 0 additions & 13 deletions
This file was deleted.

cmd/ckb/review.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ var (
4545
reviewMinReviewers int
4646
// New analyzer flags
4747
reviewStaged bool
48+
reviewNoAutoFetch bool
4849
reviewScope string
4950
reviewMaxBlastRadius int
5051
reviewMaxFanOut int
@@ -146,6 +147,7 @@ func init() {
146147

147148
// New analyzers
148149
reviewCmd.Flags().BoolVar(&reviewStaged, "staged", false, "Review staged changes instead of branch diff")
150+
reviewCmd.Flags().BoolVar(&reviewNoAutoFetch, "no-auto-fetch", false, "Disable automatic fetch of the base ref from origin when missing locally (for air-gapped CI)")
149151
reviewCmd.Flags().StringVar(&reviewScope, "scope", "", "Filter to path prefix or symbol name")
150152
reviewCmd.Flags().IntVar(&reviewMaxBlastRadius, "max-blast-radius", 0, "Maximum blast radius delta (0 = disabled)")
151153
reviewCmd.Flags().IntVar(&reviewMaxFanOut, "max-fanout", 0, "Maximum fan-out / caller count (0 = disabled)")
@@ -225,11 +227,14 @@ func runReview(cmd *cobra.Command, args []string) {
225227
Policy: policy,
226228
Checks: reviewChecks,
227229
SkipChecks: reviewSkipChecks,
228-
Staged: reviewStaged,
229-
Scope: scope,
230-
LLM: reviewLLM,
230+
Staged: reviewStaged,
231+
Scope: scope,
232+
LLM: reviewLLM,
233+
NoAutoFetch: reviewNoAutoFetch,
231234
}
232235

236+
warnIfLIPIndexEmpty(engine, repoRoot)
237+
233238
response, err := engine.ReviewPR(ctx, opts)
234239
if err != nil {
235240
fmt.Fprintf(os.Stderr, "Error running review: %v\n", err)
@@ -311,6 +316,27 @@ func runReview(cmd *cobra.Command, args []string) {
311316
}
312317
}
313318

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+
314340
// --- Output Formatters ---
315341

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

docs/plans/review-cicd.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,51 @@ jobs:
673673
sarif: true
674674
```
675675
676+
### Troubleshooting: Shallow CI Clones
677+
678+
Azure Pipelines, GitHub Actions und GitLab machen standardmäßig einen
679+
**shallow, single-branch Checkout** — nur der PR-Branch landet lokal, der
680+
Base-Branch (z.B. `main`) fehlt. Vor 9.0.1 scheiterte `ckb review --base=main`
681+
dann mit `git command failed: exit status 128`.
682+
683+
**Ab 9.0.1:** `ckb review` holt den fehlenden Base-Ref automatisch per
684+
`git fetch origin <branch>` von origin, sobald er lokal nicht auflösbar ist.
685+
In den meisten Pipelines funktioniert das ohne Änderung — der Auth-Token aus
686+
dem Checkout-Step wird wiederverwendet.
687+
688+
**Wenn der Auto-Fetch fehlschlägt:**
689+
690+
| Fehlermeldung | Ursache | Fix |
691+
|---|---|---|
692+
| `Authentication failed` / `could not read Username` / `401` / `403` | Checkout hat den Token nach dem Clone entfernt | Checkout-Step mit `persistCredentials: true` (Azure) oder `persist-credentials: true` (GitHub Actions) konfigurieren |
693+
| `Permission denied (publickey)` | SSH-Key nicht im Agent | SSH-Key zum CI-Agent hinzufügen oder HTTPS-Remote verwenden |
694+
| `couldn't find remote ref <X>` | Branch auf origin gelöscht oder falsch geschrieben | `--base` prüfen |
695+
696+
**Air-gapped Pipelines:** In Umgebungen, wo Netzwerk-Calls ausserhalb des
697+
Checkout-Steps verboten sind, Auto-Fetch deaktivieren:
698+
699+
```bash
700+
# Vor dem Review: Base-Ref explizit holen
701+
git fetch origin main:refs/heads/main
702+
ckb review --no-auto-fetch --base=main
703+
```
704+
705+
**Alternative:** Shallow-Checkout ganz vermeiden. Für GitHub Actions:
706+
707+
```yaml
708+
- uses: actions/checkout@v4
709+
with:
710+
fetch-depth: 0
711+
```
712+
713+
Für Azure Pipelines:
714+
715+
```yaml
716+
- checkout: self
717+
fetchDepth: 0
718+
persistCredentials: true
719+
```
720+
676721
## Phase 7: Baseline & Finding Lifecycle — `ckb review baseline`
677722

678723
Inspiriert von Qodana, PVS-Studio und Trunk: Findings werden nicht nur als "da/nicht da" behandelt, sondern haben einen Lifecycle.

internal/backends/git/adapter.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,46 @@ func (g *GitAdapter) Capabilities() []string {
120120
}
121121
}
122122

123+
// isAuthError returns true if the error looks like git failed to authenticate
124+
// against the remote. Used to give the user an actionable message instead of
125+
// a raw git stderr dump.
126+
func isAuthError(err error) bool {
127+
if err == nil {
128+
return false
129+
}
130+
s := strings.ToLower(err.Error())
131+
for _, marker := range []string{
132+
"authentication failed",
133+
"could not read username",
134+
"could not read password",
135+
"terminal prompts disabled",
136+
"403 forbidden",
137+
"401 unauthorized",
138+
"permission denied (publickey)",
139+
} {
140+
if strings.Contains(s, marker) {
141+
return true
142+
}
143+
}
144+
// "repository '...' not found" is typically an auth failure on private repos.
145+
if strings.Contains(s, "repository") && strings.Contains(s, "not found") {
146+
return true
147+
}
148+
return false
149+
}
150+
151+
// VerifyRef returns nil iff ref resolves to a commit in the local repo.
152+
// Unlike EnsureRef it never fetches. Use when auto-fetch is disabled.
153+
func (g *GitAdapter) VerifyRef(ref string) error {
154+
if ref == "" {
155+
return fmt.Errorf("empty ref")
156+
}
157+
if _, err := g.executeGitCommand("rev-parse", "--verify", "--quiet", ref+"^{commit}"); err != nil {
158+
return fmt.Errorf("ref %q not present locally (auto-fetch disabled)", ref)
159+
}
160+
return nil
161+
}
162+
123163
// EnsureRef returns a locally-resolvable form of the given ref, fetching
124164
// from origin if needed. Handles shallow CI clones that only fetch the PR
125165
// branch (Azure Pipelines, GitHub Actions defaults, GitLab default, etc.) —
@@ -145,6 +185,11 @@ func (g *GitAdapter) EnsureRef(ref string) (string, error) {
145185

146186
g.logger.Info("Base ref not found locally; fetching from origin", "ref", ref, "branch", branch)
147187
if _, err := g.executeGitCommand("fetch", "--no-tags", "origin", branch); err != nil {
188+
if isAuthError(err) {
189+
return "", fmt.Errorf("ref %q not found locally and fetching from origin failed due to missing credentials. "+
190+
"Your CI checkout likely stripped the auth token — enable persistCredentials (Azure Pipelines, GitHub Actions) "+
191+
"or run `ckb review --no-auto-fetch` with a pre-fetched base ref. Underlying error: %w", ref, err)
192+
}
148193
return "", fmt.Errorf("ref %q not found locally and `git fetch origin %s` failed: %w", ref, branch, err)
149194
}
150195

0 commit comments

Comments
 (0)