feat(query): gate LIP semantic rerank on !MixedModels#208
Conversation
When the LIP index contains vectors from more than one embedding model (e.g. during a partial re-index after a model upgrade), cosine similarity across those vector spaces is mathematically meaningless. The MixedModels flag was already surfaced in `ckb doctor` but no query path consumed it — so RerankWithLIP and SemanticSearchWithLIP silently ranked on garbage. Adds a cached health check on Engine (60 s TTL) that short-circuits both semantic call sites when the daemon reports MixedModels, falling back to pure lexical ranking. Surfaces the state via a `lip_mixed_models` DegradationWarning so users learn why semantic ranking is inactive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🟢 Change Impact Analysis
Blast Radius: 0 modules, 0 files, 0 unique callers 📝 Changed Symbols (4)
Recommendations
Generated by CKB |
NFR Tests ✅ 39 unchangedComparing PR against main branch (dynamic baseline). Regressions: 0 ✅ Thresholds: WARN ≥ +5% • FAIL ≥ +10% All scenarios
* = new scenario, compared against static baseline |
CKB Analysis
Risk factors: Touches 1 hotspot(s) 👥 Suggested: @lisa.welsch1985@gmail.com (100%), @talantyyr@gmail.com (40%), @lisa@tastehub.io (40%)
🎯 Change Impact Analysis · 🟢 LOW · 4 changed → 0 affected
Symbols changed in this PR:
Recommendations:
💣 Blast radius · 0 symbols · 1 tests · 0 consumersTests that may break:
🔥 Hotspots · 1 volatile files
📊 Complexity · 1 violations
💡 Quick wins · 10 suggestions
📚 Stale docs · 197 broken references
Generated by CKB · Run details |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #208 +/- ##
=====================================
Coverage 43.0% 43.0%
=====================================
Files 525 526 +1
Lines 81073 81101 +28
=====================================
+ Hits 34896 34935 +39
+ Misses 43709 43702 -7
+ Partials 2468 2464 -4
Flags with carried forward coverage won't be shown. Click here to find out more. 📢 Thoughts on this report? Let us know! 🚀 New features to boost your workflow:
|
|
CKB review failed to generate output. |
CKB used to re-probe IndexStatus on a 60 s TTL every time the rerank path checked whether the LIP index was mixed-models. The LIP daemon has pushed IndexChanged frames to all active sessions since v1.5.0, so the polling was pure debt. New internal/lip/subscribe.go opens a long-lived connection, pings IndexStatus every 3 s to flush the daemon's broadcast channel (the session loop drains queued pushes only after writing a response), reads every frame in a loop, and routes index_changed and index_status by type tag. Reconnects with exponential backoff to 30 s on daemon drop. Engine owns one subscriber, started in NewEngine and cancelled in Close. The cached availability/mixed flags are now written by the subscriber, not the query path — lipSemanticAvailable is lock-free RLock+read, no RPC. Worst-case staleness for the rerank gate drops from 60 s to ~3 s. Tests rewritten: the fake daemon now serves multiple requests per connection and tests wait for the first health frame before asserting, plus a new TestLipSubscriber_ReusesSingleConnection verifies the hot path issues zero requests. Also: CHANGELOG entries for #208 and #209 which landed on develop without one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(lip): push-driven health via long-lived subscribe CKB used to re-probe IndexStatus on a 60 s TTL every time the rerank path checked whether the LIP index was mixed-models. The LIP daemon has pushed IndexChanged frames to all active sessions since v1.5.0, so the polling was pure debt. New internal/lip/subscribe.go opens a long-lived connection, pings IndexStatus every 3 s to flush the daemon's broadcast channel (the session loop drains queued pushes only after writing a response), reads every frame in a loop, and routes index_changed and index_status by type tag. Reconnects with exponential backoff to 30 s on daemon drop. Engine owns one subscriber, started in NewEngine and cancelled in Close. The cached availability/mixed flags are now written by the subscriber, not the query path — lipSemanticAvailable is lock-free RLock+read, no RPC. Worst-case staleness for the rerank gate drops from 60 s to ~3 s. Tests rewritten: the fake daemon now serves multiple requests per connection and tests wait for the first health frame before asserting, plus a new TestLipSubscriber_ReusesSingleConnection verifies the hot path issues zero requests. Also: CHANGELOG entries for #208 and #209 which landed on develop without one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: drop stray cmd/ckb-bench/version_test.go Had a package-level init() that printed cartographer version on import under the cartographer build tag — not a test, and leaked output into unrelated test runs when the tag was enabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Engine.lipSemanticAvailable()— a 60 s TTL-cached probe oflip.IndexStatus()— and gates both semantic call sites insearchSymbols(SemanticSearchWithLIPfallback atsymbols.go:519,RerankWithLIPat:646) behind it. Mixed-model state → pure lexical fallback.lip_mixed_modelsDegradationWarning(capability 70%) so users learn why semantic ranking is inactive instead of quietly getting worse results.No change to the
internal/lipclient, the LIP daemon protocol, or the rerank function signatures — the gate is entirely on theEngineside.Test plan
go test ./internal/query/... -run 'TestLipSemanticAvailable|TestGetDegradationWarnings' -v— 6 new tests covering healthy, mixed, daemon-down, TTL-cache, warning-emitted, and no-warning-before-first-probe.go test ./internal/query/...— full package suite still passes.go build ./...clean.gofmtclean.ckb doctoragainst a real LIP daemon — confirm MixedModels reporting unchanged.searchSymbolswith single-model index (rerank fires) vs. induced mixed-model index (rerank skipped, warning present in response metadata).🤖 Generated with Claude Code