Skip to content

Cloud sweep: package doctests in CI, wheel ships skills, pgvector 0.5 fix, LanceDB native hybrid, native async registry, setup skill - #25

Merged
thorwhalen merged 11 commits into
masterfrom
cloud-sweep-2026-09-26
Sep 26, 2026
Merged

thorwhalen merged 11 commits into
masterfrom
cloud-sweep-2026-09-26

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Cloud sweep of 2026-09-26. Every change was tested in the cloud VM; see the counts below.

Tests

Command (CI's exact invocation):

python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' --ignore=examples --ignore=scrap
Environment Baseline (master) Final (this PR)
CI-shaped: Python 3.10, test extra only 134 passed, 325 skipped 210 passed, 354 skipped
Dev: .[test,dev], Python 3.11 297 passed, 162 skipped (next row)
Dev + backend extras + live Redis 8, Elasticsearch 9, Postgres 17 + pgvector, Milvus Lite not run 503 passed, 65 skipped

The baseline never collected the package's own doctests; the final counts include them. The live servers ran in Docker inside the VM, pulled from non-Docker-Hub registries (Docker Hub was rate-limited, so Weaviate, MongoDB Atlas Local and a Qdrant server were not exercised by me; the reviewer ran a temporary Qdrant server).

Dependents, with this branch installed editable over each:

Dependent Baseline Final
thorwhalen/ef 111 passed 111 passed
i2mint/ir 583 passed, 5 skipped 583 passed, 5 skipped
thorwhalen/newsmood 192 passed, 6 failed, 2 skipped 192 passed, 6 failed, 2 skipped

newsmood's 6 failures are identical before and after: tests/test_clusters.py raises a config2py missing-config error unrelated to vd.

Changes

  • test: run the package's doctests in CI (testpaths gains vd; root conftest.py skips backend modules whose SDK is missing); fix two stale doctests in vd/text.py.
  • feat(lancedb): native hybrid search through LanceDB's built-in FTS index.
  • feat(async): native async backend registry (register_async_backend, list_async_backends, connect_async(native=...)), AsyncAbstractCollection / AsyncAbstractClient bases, native qdrant adapter; shared _CollectionPolicy mixin.
  • fix(pgvector): reading documents failed with pgvector-python 0.5.
  • feat: vd-setup-backend skill; install_command returns the vd[<backend>] extra; chooser and search skills refreshed; spec-clean skill frontmatter.
  • fix: vd.__version__ from package metadata; authors filled.
  • docs: AI-first README with every example executed by tests/test_readme.py; CLAUDE.md refreshed.
  • fix(build): the wheel shipped no skills; sdist only-include.
  • fix: adversarial-review findings, round 1: qdrant's native async client is used only with a server (url=), since the embedded one blocked the event loop; LanceDB first-call index race; sync lexical_search on native async collections; a warning when lexical_search= is ignored on a native-hybrid path; a Windows path fix in the sdist test.
  • fix: review round 2: LanceDB index cache is module-level per table, invalidated on drop, and rebuilt once on a missing-index error; an http(s):// qdrant location= counts as server mode and forwards api_key and client kwargs.
  • fix(lancedb): recover from a stale index cache on lancedb 0.25 and older too, which raise RuntimeError there. Verified on 0.25.0 and 0.39.0.

Issues

Not done, and why

Adversarial review (Opus subagent)

Three rounds by a separate Opus subagent, briefed to refute the change. It reproduced every suspicion before reporting it. Final verdict: APPROVE. No round had a blocking finding.

Round 1 (APPROVE, 8 non-blocking):

  1. Native async qdrant blocked the event loop in embedded mode (157 ms vs 7 ms). Fixed: native only for url= or an http(s) location=; I measured the loop never getting a turn with the embedded native client. Docs corrected.
  2. Concurrent first LanceDB hybrid calls raced creating the index (7 of 8 threads failed). Fixed with a lock, a re-check after conflicts, and a concurrency test.
  3. Sync lexical_search callables broke on native async collections. Fixed: they receive a mapping of the documents.
  4. lexical_search= was silently ignored on native-hybrid backends, now including LanceDB. Fixed: it warns.
  5. CI installs no backend clients, so backend-specific tests skip in CI. Not changed: left for the maintainer; see "For whoever lands it".
  6. The sdist test would fail on Windows path separators. Fixed.
  7. Small private-API drift: AbstractCollection.supported_filter_operators on the base class itself, removed QdrantCollection._payload, ._point and ._to_document. Accepted: private, and no dependent uses them.
  8. The index check ran on every query. Fixed: cached.

Round 2 (APPROVE): the index cache went stale after a drop and recreate, and the lock was contended for per-request collection objects. Fixed in round 2's commit. The warning's stack location on async paths is accepted as is.

Round 3 (APPROVE): older LanceDB raises RuntimeError for the missing index. Fixed in the last commit, a narrow follow-up verified on both versions. A table dropped by another thread mid-search surfaces LanceDB's own not-found error. Accepted: that is concurrent misuse, not caused by this PR.

For whoever lands it

  • Merging publishes a release. It is the first wheel that actually contains the bundled skills.
  • install_command() output changed from raw client names to pip install "vd[<backend>]". No dependent parses it.
  • connect_async("qdrant", url=...) now returns a native client (native_async is True, no .sync attribute); native=False gives the old wrapper. Embedded qdrant is unchanged.
  • CI installs only the test extra, so backend-specific tests (qdrant, LanceDB, pgvector, live servers) skip there. They were run in this VM. Consider a CI extra with qdrant-client and lancedb, both embedded and server-free.

Fixes #24
Fixes #10
Fixes #26
Fixes #27
Fixes #28

🤖 Generated with Claude Code

CI invoked pytest with no path, so testpaths=["tests"] meant none of the
~290 doctest lines under vd/ ever ran. Add `vd` to testpaths and a root
conftest.py that skips backend modules whose optional SDK is missing (and
misc/ demo scripts), so collection never aborts on an ImportError.

Two doctests in vd/text.py had wrong expected output (clean_text with
remove_punctuation, extract_metadata char_count); the code was right.

Fixes #24

Copy link
Copy Markdown
Member Author

cloud-status: started — baseline 297/0/162 (dev env; 134/0/325 in CI's test-only env); plan: run package doctests in CI (#24), setup-backend skill (#10), LanceDB native hybrid (#17), async registry + native qdrant (#20), README/CLAUDE.md refresh


Generated by Claude Code

LanceDBCollection now satisfies SupportsHybrid. The lexical side runs on
LanceDB's native BM25 full-text index over `text` (no tantivy needed),
built on the first hybrid call; rows written later are still searched.
Fusion stays vd's client-side RRF, so the fused score matches every other
backend. Filters apply client-side as on the dense side.

Refs #17 (lancedb item; the other five backends remain)
connect_async now dispatches through a registry of native async clients
(register_async_backend / list_async_backends) and falls back to the
to_thread wrapper otherwise; native=False forces the wrapper.

New bases AsyncAbstractCollection / AsyncAbstractClient give native
adapters the same user-facing surface as the sync ones. The I/O-free
embedding / dimension / query-resolution policy moved from
AbstractCollection into a shared _CollectionPolicy mixin so both bases
use one implementation (sync behaviour unchanged).

qdrant is the first native backend (qdrant_client.AsyncQdrantClient),
sharing its filter compiler and point converters with the sync adapter.
Tested in embedded :memory: mode, including a parity test against the
wrapped sync adapter. hybrid_search_async now works on native collections
(native hybrid if present, else async dense + client-side BM25 + RRF).

The vd-add-backend dev skill documents the native hybrid and native async
hooks, and names the real conftest backend lists.

Refs #20 (qdrant done; nine backends remain)
…0.5)

pgvector-python 0.5 returns its own non-iterable Vector type from the
psycopg adapter, so every document read raised "'Vector' object is not
iterable". Convert through to_list()/tolist() when available. Verified
against a live Postgres 17 + pgvector server (30 pgvector tests pass,
10 failed before); a server-free unit test guards the conversion.

Fixes #26
New user skill vd-setup-backend: the diagnose -> act -> connect ->
smoke-test loop, per-archetype start-up (embedded persistence kwargs,
Docker one-liners, managed credentials and which adapters read them from
the environment), and troubleshooting. Every snippet was run: embedded
backends, Milvus Lite, and live Redis 8, Elasticsearch 9 and Postgres 17 +
pgvector servers.

install_command() now returns `pip install "vd[<backend>]"` for every
backend with an adapter, so the printed command installs exactly what the
adapter imports (pyproject extras are the single source of truth). The old
table had drifted: pgvector omitted psycopg[binary] and milvus omitted
milvus-lite. install_backend(run=True) parses the quoted command with
shlex. A test checks every adapter has a matching extra.

vd-backend-choose now covers choosing only (setup moves to the new
skill) and lists native-hybrid and native-async backends. vd-search gains
a hybrid-search section and drops a stale claim that chroma returns
distances. All skills use spec-clean frontmatter (audience under
metadata).

Fixes #10
__version__ was a literal "0.2.0" that CI's version bump never touches,
so it was stale on every release (0.2.11 today). Read it from
importlib.metadata instead. Also fill the empty `authors` field and add a
test that every bundled skill's frontmatter is spec-clean.

Fixes #27
README now opens with what vd does and an agent section (skills via gh
skill or the pip-shipped folder, where CLAUDE.md is, a runnable example),
then the essentials, including the new hybrid and async sections, and
ends with a contributor section. Every Python example is executed by
tests/test_readme.py, which also checks the printed output.

CLAUDE.md gains how to run the tests (CI's exact command, the live
server suite), the async and hybrid architecture, and a status line on
the refactor priorities.
.claude/skills/* symlink into vd/data/skills/. Hatch walks .claude first
and skips files whose real path it has already seen, so the sdist kept
only the .claude copies and the wheel built from it carried no skills
(true of the published 0.2.11). Give the sdist an explicit only-include
list so .claude is never walked, and test the sdist file list through
hatchling (added to the test extra so CI runs it).

Fixes #28
- qdrant: connect_async returns the native client only with url=. In
  embedded mode qdrant-client's async client runs blocking code inside
  its coroutines (measured: the loop never got a turn during searches),
  so embedded mode keeps the thread-pool wrapper. Docs corrected.
- lancedb: concurrent first hybrid calls raced creating the FTS index
  (Lance commit conflict). Creation is now serialized, a conflict is
  tolerated when the index exists afterwards, the table is reopened so
  handles opened earlier see the index, and the check is cached.
- hybrid_search_async on native collections accepts sync lexical_search
  callables again (they get a mapping of the documents); async ones get
  the collection.
- hybrid_search warns instead of silently ignoring lexical_search= when
  the collection runs hybrid natively.
- The sdist test normalizes path separators for the Windows job.

Refs #17, #20
…ocation)

- lancedb: the "FTS index ready" cache is now module-level per
  (database, table), checked before taking the creation lock, dropped
  on delete_collection, and a missing-index error on search forgets the
  entry and rebuilds once. Fixes a stale cache after a table is dropped
  and recreated, and lock contention for per-request collection objects.
- qdrant: an http(s):// location= counts as server mode for
  connect_async, and gets api_key and extra client kwargs like url= does
  (the sync adapter dropped them too).
- hybrid_search_async documents what sync vs async lexical_search
  callables receive on native collections.

Refs #17, #20
lancedb <= 0.25 raises RuntimeError (not ValueError) when full-text
search finds no INVERTED index. Catch both, only for that message, so
the one-shot rebuild after a table was dropped behind vd's back works on
every supported release (verified on 0.25.0 and 0.39.0).

Refs #17
@thorwhalen thorwhalen changed the title WIP: Cloud sweep (2026-09-26) Cloud sweep: package doctests in CI, wheel ships skills, pgvector 0.5 fix, LanceDB native hybrid, native async registry, setup skill Sep 26, 2026
@thorwhalen
thorwhalen merged commit ee58c41 into master Sep 26, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the cloud-sweep-2026-09-26 branch September 26, 2026 10:51

Copy link
Copy Markdown
Member Author

cloud-status: done — tests 134→210 passed in CI's env (297→503 in dev + live servers), dependents unchanged; closed #24 #10 #26 #27 #28 (filed #24 #26 #27 #28), needs-local #17 #20; merged, vd 0.2.12 published (wheel now ships the 7 skills)


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment