diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 404927f..a7607ec 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -11,7 +11,9 @@ {"id":"Pommel-29q","title":"Phase 22: PowerShell Install Script","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T15:42:44.101619-07:00","updated_at":"2026-01-03T12:34:02.967626-07:00","closed_at":"2026-01-03T12:34:02.967626-07:00","close_reason":"Completed in v0.5.2 release","dependencies":[{"issue_id":"Pommel-29q","depends_on_id":"Pommel-4f3","type":"blocks","created_at":"2025-12-30T15:43:22.288035-07:00","created_by":"ryan"}]} {"id":"Pommel-2bv","title":"Write tests for SearchTimeMs tracking","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:28:20.297661-07:00","updated_at":"2025-12-29T08:47:39.638123-07:00","closed_at":"2025-12-29T08:47:39.638123-07:00","close_reason":"All features implemented and tested in v0.2.0"} {"id":"Pommel-2jw","title":"E2E verification of Java support","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T18:36:13.933031-07:00","updated_at":"2025-12-29T18:52:14.62392-07:00","closed_at":"2025-12-29T18:52:14.62392-07:00","close_reason":"All 17 Java chunker tests pass. Java support verified for: classes, interfaces, enums, records, annotation types, methods, constructors, nested classes, generics.","dependencies":[{"issue_id":"Pommel-2jw","depends_on_id":"Pommel-yi4","type":"blocks","created_at":"2025-12-29T18:36:47.668428-07:00","created_by":"ryan"}]} +{"id":"Pommel-2uyr","title":"Phase 5: Finalization","description":"Version bump, full test suite, PR creation","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-15T21:29:54.165991-07:00","created_by":"ryan","updated_at":"2026-01-15T22:23:12.577472-07:00","closed_at":"2026-01-15T22:23:12.577472-07:00","close_reason":"Phase 5 complete: version bump, tests pass, review approved"} {"id":"Pommel-31d","title":"8.3 Write config port override tests","description":"Write tests for config port override taking precedence over hash calculation","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:29:49.283944-07:00","updated_at":"2026-01-01T13:41:00.16277-07:00","closed_at":"2026-01-01T13:41:00.16277-07:00","close_reason":"Completed in previous session"} +{"id":"Pommel-323f","title":"Phase 1: Model Registry","description":"Create model registry with ModelInfo struct mapping short names (v2/v4) to full Ollama model names and dimensions","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-15T21:29:47.191583-07:00","created_by":"ryan","updated_at":"2026-01-15T21:47:30.248742-07:00","closed_at":"2026-01-15T21:47:30.248742-07:00","close_reason":"Phase 1 complete: All 4 model registry functions implemented"} {"id":"Pommel-32n","title":"4.2a: Write Indexer tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T20:17:40.164512-07:00","updated_at":"2025-12-28T20:34:53.705386-07:00","closed_at":"2025-12-28T20:34:53.705386-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-32n","depends_on_id":"Pommel-nox","type":"blocks","created_at":"2025-12-28T20:19:07.143766-07:00","created_by":"ryan"}]} {"id":"Pommel-334","title":"Add ChunkLevelSection to models","description":"Add new chunk level for document formats (markdown, yaml)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:29:46.417019-07:00","updated_at":"2026-01-03T12:35:49.462635-07:00","closed_at":"2026-01-03T12:35:49.462635-07:00","close_reason":"Closed"} {"id":"Pommel-37c","title":"Add error logging for background reindex failures","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-29T08:29:09.253282-07:00","updated_at":"2025-12-29T08:47:39.643365-07:00","closed_at":"2025-12-29T08:47:39.643365-07:00","close_reason":"All features implemented and tested in v0.2.0"} @@ -46,6 +48,7 @@ {"id":"Pommel-5g7","title":"3.3a: Write C# Chunker tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:48:18.559316-07:00","updated_at":"2025-12-28T20:03:00.861712-07:00","closed_at":"2025-12-28T20:03:00.861712-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-5g7","depends_on_id":"Pommel-c47","type":"blocks","created_at":"2025-12-28T19:50:00.2682-07:00","created_by":"ryan"},{"issue_id":"Pommel-5g7","depends_on_id":"Pommel-bwg","type":"blocks","created_at":"2025-12-28T19:50:05.455511-07:00","created_by":"ryan"}]} {"id":"Pommel-5gp","title":"5.2b: Implement DB Search methods","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T21:05:47.904462-07:00","updated_at":"2025-12-28T21:15:37.289133-07:00","closed_at":"2025-12-28T21:15:37.289133-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-5gp","depends_on_id":"Pommel-d4y","type":"blocks","created_at":"2025-12-28T21:07:41.179081-07:00","created_by":"ryan"}]} {"id":"Pommel-5ha","title":"13.3 Handle monorepo prompting","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:17:06.936423-07:00","updated_at":"2025-12-29T12:22:47.923637-07:00","closed_at":"2025-12-29T12:22:47.923637-07:00","close_reason":"Closed"} +{"id":"Pommel-5uix","title":"1.1: GetModelInfo function","description":"Create GetModelInfo(shortName) returning ModelInfo with Name, Dimensions, ContextSize, Size fields. Tests: TestGetModelInfo_V2, TestGetModelInfo_V4, TestGetModelInfo_UnknownShortName, TestGetModelInfo_EmptyName, TestGetModelInfo_CaseInsensitive","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:54.772106-07:00","created_by":"ryan","updated_at":"2026-01-15T21:38:21.353769-07:00","closed_at":"2026-01-15T21:38:21.353769-07:00","close_reason":"Task 1.1 complete: GetModelInfo function implemented with TDD, spec reviewed, code quality reviewed"} {"id":"Pommel-5y6","title":"TDD tests for code generator","description":"Tests for generator: happy path, failure, error, edge cases","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:30:11.580218-07:00","updated_at":"2026-01-03T13:03:21.283508-07:00","closed_at":"2026-01-03T13:03:21.283508-07:00","close_reason":"Existing tests cover the functionality - chunker and status tests all passing","dependencies":[{"issue_id":"Pommel-5y6","depends_on_id":"Pommel-ek9","type":"blocks","created_at":"2026-01-03T12:31:43.177845-07:00","created_by":"ryan"}]} {"id":"Pommel-60u","title":"Phase 1: Provider Abstraction Foundation","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T10:32:55.743467-07:00","created_by":"ryan","updated_at":"2026-01-04T10:44:27.041678-07:00","closed_at":"2026-01-04T10:44:27.041678-07:00","close_reason":"Phase 1 implementation complete with ~60 tests"} {"id":"Pommel-64q","title":"Write tests for --auto flag","description":"TDD: Write tests for pm init --auto flag that auto-detects project languages and configures include patterns accordingly. Tests should verify: 1) Detection of Go files adds *.go pattern 2) Detection of Python adds *.py 3) Detection of JS/TS adds appropriate patterns 4) Multiple languages detected correctly 5) Flag registers properly on command","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T07:58:47.830101-07:00","updated_at":"2025-12-29T08:06:48.624703-07:00","closed_at":"2025-12-29T08:06:48.624703-07:00","close_reason":"All init flags implemented and tested via TDD"} @@ -60,6 +63,7 @@ {"id":"Pommel-6u6.2","title":"5.3b: Implement Search API Integration","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T21:07:20.557139-07:00","updated_at":"2025-12-28T21:27:14.085449-07:00","closed_at":"2025-12-28T21:27:14.085449-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-6u6.2","depends_on_id":"Pommel-6u6","type":"parent-child","created_at":"2025-12-28T21:07:20.557722-07:00","created_by":"ryan"},{"issue_id":"Pommel-6u6.2","depends_on_id":"Pommel-6u6.1","type":"blocks","created_at":"2025-12-28T21:07:56.777605-07:00","created_by":"ryan"}]} {"id":"Pommel-7ag","title":"Implement pending_changes in status response","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:28:51.031485-07:00","updated_at":"2025-12-29T08:47:39.63673-07:00","closed_at":"2025-12-29T08:47:39.63673-07:00","close_reason":"All features implemented and tested in v0.2.0","dependencies":[{"issue_id":"Pommel-7ag","depends_on_id":"Pommel-3e9","type":"blocks","created_at":"2025-12-29T08:30:15.897841-07:00","created_by":"ryan"}]} {"id":"Pommel-7ee","title":"Implement pm config set search.default_levels","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:28:13.778211-07:00","updated_at":"2025-12-29T08:47:39.638555-07:00","closed_at":"2025-12-29T08:47:39.638555-07:00","close_reason":"All features implemented and tested in v0.2.0","dependencies":[{"issue_id":"Pommel-7ee","depends_on_id":"Pommel-dde","type":"blocks","created_at":"2025-12-29T08:30:05.525143-07:00","created_by":"ryan"}]} +{"id":"Pommel-7fzc","title":"3.2: Switch model","description":"Implement 'pm config model v4' to switch models - validate target, check if same, delete DB if exists, update config. Tests: TestConfigModel_SetV4_NoExistingDB","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:54.044983-07:00","created_by":"ryan","updated_at":"2026-01-15T22:07:58.880904-07:00","closed_at":"2026-01-15T22:07:58.880904-07:00","close_reason":"Task 3.2 complete: Model switching with DB cleanup, TDD, reviewed","dependencies":[{"issue_id":"Pommel-7fzc","depends_on_id":"Pommel-nhog","type":"blocks","created_at":"2026-01-15T21:31:05.408788-07:00","created_by":"ryan"}]} {"id":"Pommel-7hw","title":"4.3b: Implement State Management","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T20:18:08.049366-07:00","updated_at":"2025-12-28T20:29:48.858165-07:00","closed_at":"2025-12-28T20:29:48.858165-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-7hw","depends_on_id":"Pommel-bjm","type":"blocks","created_at":"2025-12-28T20:19:00.855595-07:00","created_by":"ryan"}]} {"id":"Pommel-897","title":"Windows installer: remove hardcoded language list, use dynamic discovery","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T09:22:16.261089-07:00","updated_at":"2026-01-01T09:24:33.56421-07:00","closed_at":"2026-01-01T09:24:33.56421-07:00","close_reason":"Implemented in commit 4bf7e2a"} {"id":"Pommel-8gn","title":"15.2 Implement missing features","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:26:32.719816-07:00","updated_at":"2025-12-29T12:29:32.519136-07:00","closed_at":"2025-12-29T12:29:32.519136-07:00","close_reason":"Closed"} @@ -82,8 +86,11 @@ {"id":"Pommel-9gf","title":"Phase 28: Re-ranker","description":"Implement two-tier re-ranking: heuristic signals and Ollama cross-encoder with automatic fallback. --no-rerank flag.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T11:27:48.590986-07:00","updated_at":"2026-01-01T11:59:53.747732-07:00","closed_at":"2026-01-01T11:59:53.747732-07:00","close_reason":"Phase 28 re-ranker complete with heuristic scoring signals","dependencies":[{"issue_id":"Pommel-9gf","depends_on_id":"Pommel-ubg","type":"blocks","created_at":"2026-01-01T11:28:21.064113-07:00","created_by":"ryan"},{"issue_id":"Pommel-9gf","depends_on_id":"Pommel-4xu","type":"blocks","created_at":"2026-01-01T11:28:26.252101-07:00","created_by":"ryan"}]} {"id":"Pommel-9mk","title":"8.5 Write health endpoint tests","description":"Write tests for /health endpoint returning project_root and version","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:30:02.060319-07:00","updated_at":"2025-12-29T11:49:03.239708-07:00","closed_at":"2025-12-29T11:49:03.239708-07:00","close_reason":"Phase 8 health endpoint and port hashing implemented and tested"} {"id":"Pommel-9ne","title":"Phase 5: Polish and Migration","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T10:33:01.902394-07:00","created_by":"ryan","updated_at":"2026-01-04T11:10:53.085142-07:00","closed_at":"2026-01-04T11:10:53.085142-07:00","close_reason":"Phase 5 complete: ETA calculation, retry logic, docs updated","dependencies":[{"issue_id":"Pommel-9ne","depends_on_id":"Pommel-mqn","type":"blocks","created_at":"2026-01-04T10:33:10.269804-07:00","created_by":"ryan"}]} +{"id":"Pommel-9vul","title":"5.3: Create PR","description":"Push branch, create PR to dev with full description","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:56.810099-07:00","created_by":"ryan","updated_at":"2026-01-15T22:23:12.541678-07:00","closed_at":"2026-01-15T22:23:12.541678-07:00","close_reason":"Final code review APPROVED with minor improvement implemented","dependencies":[{"issue_id":"Pommel-9vul","depends_on_id":"Pommel-ad3e","type":"blocks","created_at":"2026-01-15T21:31:05.512683-07:00","created_by":"ryan"}]} {"id":"Pommel-a1t","title":"Implement pm init flags (--auto, --claude, --start)","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T07:58:24.376222-07:00","updated_at":"2025-12-29T08:06:48.627724-07:00","closed_at":"2025-12-29T08:06:48.627724-07:00","close_reason":"All init flags implemented and tested via TDD"} +{"id":"Pommel-ad3e","title":"5.2: Full test suite","description":"Run go test -tags fts5 ./... and ensure all pass","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:55.936944-07:00","created_by":"ryan","updated_at":"2026-01-15T22:20:59.598223-07:00","closed_at":"2026-01-15T22:20:59.598223-07:00","close_reason":"All tests pass - go test -tags fts5 ./... runs successfully","dependencies":[{"issue_id":"Pommel-ad3e","depends_on_id":"Pommel-ecpr","type":"blocks","created_at":"2026-01-15T21:31:05.480134-07:00","created_by":"ryan"}]} {"id":"Pommel-ah2","title":"2.2a Write Embedder Interface + Mock Tests","description":"TDD: Write tests for Embedder interface and MockEmbedder - deterministic output, health toggle","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:19:55.284762-07:00","updated_at":"2025-12-28T19:33:37.239618-07:00","closed_at":"2025-12-28T19:33:37.239618-07:00","close_reason":"12 tests written, 10 failing as expected (TDD)","dependencies":[{"issue_id":"Pommel-ah2","depends_on_id":"Pommel-jro","type":"blocks","created_at":"2025-12-28T19:21:07.347859-07:00","created_by":"ryan"}]} +{"id":"Pommel-ail","title":"Jina v4 Embedding Support","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-15T21:29:12.777697-07:00","created_by":"ryan","updated_at":"2026-01-15T22:23:17.524066-07:00","closed_at":"2026-01-15T22:23:17.524066-07:00","close_reason":"Jina v4 Embedding Support feature complete - all phases implemented"} {"id":"Pommel-ayz","title":"Phase 10: Subproject Detection","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:01:11.057005-07:00","updated_at":"2025-12-29T12:06:43.693184-07:00","closed_at":"2025-12-29T12:06:43.693184-07:00","close_reason":"Phase 10 implementation complete"} {"id":"Pommel-aze","title":"8.4 Update config schema for port","description":"Update DaemonConfig.Port to pointer type for optional override","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:29:55.880187-07:00","updated_at":"2025-12-29T11:47:25.487922-07:00","closed_at":"2025-12-29T11:47:25.487922-07:00","close_reason":"Config schema updated for optional port (*int with nil = hash-based)"} {"id":"Pommel-b0g","title":"Phase 29: Enhanced Output","description":"Add score breakdown, match reasons, --verbose flag. Update JSON output with new fields.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T11:27:54.936862-07:00","updated_at":"2026-01-01T12:07:38.839648-07:00","closed_at":"2026-01-01T12:07:38.839648-07:00","close_reason":"Implemented output/reasons.go, output/formatter.go, --verbose flag, all tests pass","dependencies":[{"issue_id":"Pommel-b0g","depends_on_id":"Pommel-4xu","type":"blocks","created_at":"2026-01-01T11:28:31.436356-07:00","created_by":"ryan"},{"issue_id":"Pommel-b0g","depends_on_id":"Pommel-9gf","type":"blocks","created_at":"2026-01-01T11:28:36.637985-07:00","created_by":"ryan"}]} @@ -102,7 +109,9 @@ {"id":"Pommel-ct9","title":"2.1a Write Ollama Client Tests","description":"TDD: Write failing tests for OllamaClient - health check, embeddings, batch, errors","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:19:42.259781-07:00","updated_at":"2025-12-28T19:26:22.946463-07:00","closed_at":"2025-12-28T19:26:22.946463-07:00","close_reason":"18 tests written, 17 failing as expected (TDD)"} {"id":"Pommel-czv","title":"9.4 Implement subproject DB operations","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:51:09.974895-07:00","updated_at":"2025-12-29T12:00:17.250769-07:00","closed_at":"2025-12-29T12:00:17.250769-07:00","close_reason":"Phase 9 implementation complete - all tests passing","dependencies":[{"issue_id":"Pommel-czv","depends_on_id":"Pommel-ybs","type":"blocks","created_at":"2025-12-29T11:52:12.866373-07:00","created_by":"ryan"}]} {"id":"Pommel-d4y","title":"5.2a: Write DB Search tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T21:05:41.81473-07:00","updated_at":"2025-12-28T21:13:22.274403-07:00","closed_at":"2025-12-28T21:13:22.274403-07:00","close_reason":"Closed"} +{"id":"Pommel-d8q","title":"Phase 2: OllamaClient Updates","description":"Update OllamaClient to use model registry for dynamic dimensions and context size","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-15T21:29:46.851543-07:00","created_by":"ryan","updated_at":"2026-01-15T21:55:36.569098-07:00","closed_at":"2026-01-15T21:55:36.569098-07:00","close_reason":"Phase 2 complete: OllamaClient uses model registry for dimensions and context"} {"id":"Pommel-d8z","title":"Phase 19: Path Handling for Windows","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T15:42:24.995904-07:00","updated_at":"2026-01-03T12:34:02.969152-07:00","closed_at":"2026-01-03T12:34:02.969152-07:00","close_reason":"Completed in v0.5.2 release","dependencies":[{"issue_id":"Pommel-d8z","depends_on_id":"Pommel-4f3","type":"blocks","created_at":"2025-12-30T15:43:06.721693-07:00","created_by":"ryan"}]} +{"id":"Pommel-dca0","title":"1.3: GetDimensionsForModel function","description":"Create GetDimensionsForModel(fullName) returning int with 768 default for unknown. Tests: TestGetDimensionsForModel_V2, TestGetDimensionsForModel_V4, TestGetDimensionsForModel_Unknown_DefaultsTo768, TestGetDimensionsForModel_Empty_DefaultsTo768","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:56.943701-07:00","created_by":"ryan","updated_at":"2026-01-15T21:44:10.638417-07:00","closed_at":"2026-01-15T21:44:10.638417-07:00","close_reason":"Task 1.3 complete: GetDimensionsForModel with safe defaults, TDD, reviewed","dependencies":[{"issue_id":"Pommel-dca0","depends_on_id":"Pommel-323f","type":"parent-child","created_at":"2026-01-15T21:30:25.585992-07:00","created_by":"ryan"},{"issue_id":"Pommel-dca0","depends_on_id":"Pommel-5uix","type":"blocks","created_at":"2026-01-15T21:30:32.447803-07:00","created_by":"ryan"}]} {"id":"Pommel-dd3","title":"15.1 Audit all CLI commands","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:26:26.208417-07:00","updated_at":"2025-12-29T12:29:32.517888-07:00","closed_at":"2025-12-29T12:29:32.517888-07:00","close_reason":"Closed"} {"id":"Pommel-dde","title":"Write tests for pm config set search.default_levels","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:28:08.540408-07:00","updated_at":"2025-12-29T08:47:39.638983-07:00","closed_at":"2025-12-29T08:47:39.638983-07:00","close_reason":"All features implemented and tested in v0.2.0"} {"id":"Pommel-dfy","title":"3.1a: Write Tree-sitter integration tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:47:45.889099-07:00","updated_at":"2025-12-28T19:54:48.787392-07:00","closed_at":"2025-12-28T19:54:48.787392-07:00","close_reason":"Closed"} @@ -112,6 +121,7 @@ {"id":"Pommel-dyj","title":"10.3 Create Subproject Manager","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:01:56.715606-07:00","updated_at":"2025-12-29T12:06:43.692229-07:00","closed_at":"2025-12-29T12:06:43.692229-07:00","close_reason":"Phase 10 implementation complete","dependencies":[{"issue_id":"Pommel-dyj","depends_on_id":"Pommel-ayz","type":"blocks","created_at":"2025-12-29T12:03:06.216271-07:00","created_by":"ryan"}]} {"id":"Pommel-dz7","title":"13.2 Detect monorepo during init","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:17:00.485287-07:00","updated_at":"2025-12-29T12:22:47.923101-07:00","closed_at":"2025-12-29T12:22:47.923101-07:00","close_reason":"Closed"} {"id":"Pommel-e4k","title":"Phase 1: Foundation (tokens, minified, DB schema)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T16:52:25.555605-07:00","created_by":"ryan","updated_at":"2026-01-04T17:03:00.260428-07:00","closed_at":"2026-01-04T17:03:00.260428-07:00","close_reason":"Phase 1 complete"} +{"id":"Pommel-ecpr","title":"5.1: Version bump to 0.8.0","description":"Update version in cmd/pm/main.go, cmd/pommeld/main.go, internal/cli/root.go","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:55.15482-07:00","created_by":"ryan","updated_at":"2026-01-15T22:19:13.584786-07:00","closed_at":"2026-01-15T22:19:13.584786-07:00","close_reason":"Version bumped to 0.8.0 in cmd/pm/main.go, cmd/pommeld/main.go, and internal/cli/root.go","dependencies":[{"issue_id":"Pommel-ecpr","depends_on_id":"Pommel-p4q4","type":"blocks","created_at":"2026-01-15T21:32:08.819331-07:00","created_by":"ryan"},{"issue_id":"Pommel-ecpr","depends_on_id":"Pommel-ysb1","type":"blocks","created_at":"2026-01-15T21:32:08.852142-07:00","created_by":"ryan"},{"issue_id":"Pommel-ecpr","depends_on_id":"Pommel-hijl","type":"blocks","created_at":"2026-01-15T21:32:08.885073-07:00","created_by":"ryan"},{"issue_id":"Pommel-ecpr","depends_on_id":"Pommel-q8ie","type":"blocks","created_at":"2026-01-15T21:32:08.91829-07:00","created_by":"ryan"},{"issue_id":"Pommel-ecpr","depends_on_id":"Pommel-io83","type":"blocks","created_at":"2026-01-15T21:32:08.950402-07:00","created_by":"ryan"}]} {"id":"Pommel-ek9","title":"Create code generator for treesitter","description":"Build go generate tool that reads languages/*.yaml and produces treesitter_generated.go","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:29:52.825276-07:00","updated_at":"2026-01-03T12:58:37.473989-07:00","closed_at":"2026-01-03T12:58:37.473989-07:00","close_reason":"Code generator and chunker refactoring complete","dependencies":[{"issue_id":"Pommel-ek9","depends_on_id":"Pommel-334","type":"blocks","created_at":"2026-01-03T12:31:23.654128-07:00","created_by":"ryan"}]} {"id":"Pommel-emu","title":"Phase 3: CLI Commands","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T10:32:58.870421-07:00","created_by":"ryan","updated_at":"2026-01-04T10:59:08.578477-07:00","closed_at":"2026-01-04T10:59:08.578477-07:00","close_reason":"Phase 3 complete: CLI commands for provider configuration","dependencies":[{"issue_id":"Pommel-emu","depends_on_id":"Pommel-jcj","type":"blocks","created_at":"2026-01-04T10:33:10.20445-07:00","created_by":"ryan"}]} {"id":"Pommel-ewn","title":"2.4a Write Vector Storage Tests","description":"TDD: Write failing tests for vector insert, batch insert, delete, search, filtered search","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:20:20.644636-07:00","updated_at":"2025-12-28T19:26:28.129237-07:00","closed_at":"2025-12-28T19:26:28.129237-07:00","close_reason":"17 tests written, 16 failing as expected (TDD)"} @@ -121,30 +131,38 @@ {"id":"Pommel-gbo","title":"Phase 1: Foundation","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T18:59:05.839649-07:00","updated_at":"2025-12-28T19:14:36.25552-07:00","closed_at":"2025-12-28T19:14:36.25552-07:00","close_reason":"Phase 1 Foundation complete - all tests passing, build succeeds","dependencies":[{"issue_id":"Pommel-gbo","depends_on_id":"Pommel-l5y","type":"blocks","created_at":"2025-12-28T19:00:29.922558-07:00","created_by":"ryan"},{"issue_id":"Pommel-gbo","depends_on_id":"Pommel-yzc","type":"blocks","created_at":"2025-12-28T19:00:35.146026-07:00","created_by":"ryan"},{"issue_id":"Pommel-gbo","depends_on_id":"Pommel-3hq","type":"blocks","created_at":"2025-12-28T19:00:40.326559-07:00","created_by":"ryan"},{"issue_id":"Pommel-gbo","depends_on_id":"Pommel-90v","type":"blocks","created_at":"2025-12-28T19:00:45.506767-07:00","created_by":"ryan"},{"issue_id":"Pommel-gbo","depends_on_id":"Pommel-hgz","type":"blocks","created_at":"2025-12-28T19:00:50.686173-07:00","created_by":"ryan"}]} {"id":"Pommel-gsl","title":"Document all chunk levels in README","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-29T08:29:31.480523-07:00","updated_at":"2025-12-29T08:47:39.641771-07:00","closed_at":"2025-12-29T08:47:39.641771-07:00","close_reason":"All features implemented and tested in v0.2.0"} {"id":"Pommel-hgz","title":"1.5 CLI Skeleton - Cobra commands with version","description":"Create Cobra root command, version command, output formatter, and CLI tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T18:59:50.04926-07:00","updated_at":"2025-12-28T19:13:33.455793-07:00","closed_at":"2025-12-28T19:13:33.455793-07:00","close_reason":"Implementation complete with all tests passing","dependencies":[{"issue_id":"Pommel-hgz","depends_on_id":"Pommel-yzc","type":"blocks","created_at":"2025-12-28T19:00:24.740082-07:00","created_by":"ryan"}]} +{"id":"Pommel-hijl","title":"3.3: Error handling","description":"Handle invalid model names and same-model no-op. Tests: TestConfigModel_InvalidModel, TestConfigModel_SameModel","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:54.942414-07:00","created_by":"ryan","updated_at":"2026-01-15T22:08:14.076239-07:00","closed_at":"2026-01-15T22:08:14.076239-07:00","close_reason":"Task 3.3 complete: Error handling already implemented in Task 3.2","dependencies":[{"issue_id":"Pommel-hijl","depends_on_id":"Pommel-7fzc","type":"blocks","created_at":"2026-01-15T21:31:05.446369-07:00","created_by":"ryan"}]} {"id":"Pommel-hjh","title":"8.2 Implement port calculation function","description":"Implement CalculatePort() using FNV-1a hash in range 49152-65535","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:29:42.928451-07:00","updated_at":"2025-12-29T11:32:53.554592-07:00","closed_at":"2025-12-29T11:32:53.554592-07:00","close_reason":"Port calculation tests and implementation complete - all tests pass"} {"id":"Pommel-hux","title":"Universal Language Support (31 languages)","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-03T12:29:19.78731-07:00","updated_at":"2026-01-03T13:03:43.525719-07:00","closed_at":"2026-01-03T13:03:43.525719-07:00","close_reason":"Main features implemented - status improvements and code generator working"} {"id":"Pommel-ib7","title":"Include languages/ in release archives","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T09:22:09.971061-07:00","updated_at":"2026-01-01T09:24:33.563203-07:00","closed_at":"2026-01-01T09:24:33.563203-07:00","close_reason":"Implemented in commit 4bf7e2a"} +{"id":"Pommel-io83","title":"4.2: PowerShell install script","description":"Add Select-OllamaModel function, update Write-GlobalConfig and Setup-EmbeddingModel for model selection","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:53.256575-07:00","created_by":"ryan","updated_at":"2026-01-15T22:17:46.818665-07:00","closed_at":"2026-01-15T22:17:46.818665-07:00","close_reason":"Task 4.2 complete: PowerShell install script with model selection, reviewed","dependencies":[{"issue_id":"Pommel-io83","depends_on_id":"Pommel-5uix","type":"blocks","created_at":"2026-01-15T21:31:22.653832-07:00","created_by":"ryan"}]} {"id":"Pommel-jcj","title":"Phase 2: Global Config Support","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T10:32:57.506635-07:00","created_by":"ryan","updated_at":"2026-01-04T10:51:00.304126-07:00","closed_at":"2026-01-04T10:51:00.304126-07:00","close_reason":"Phase 2 complete: global config, migration, and validation","dependencies":[{"issue_id":"Pommel-jcj","depends_on_id":"Pommel-60u","type":"blocks","created_at":"2026-01-04T10:33:10.171568-07:00","created_by":"ryan"}]} {"id":"Pommel-jkd","title":"2.3b Implement Query Cache","description":"TDD: Implement CachedEmbedder with LRU cache to pass all tests from 2.3a","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:20:14.120489-07:00","updated_at":"2025-12-28T19:41:10.145821-07:00","closed_at":"2025-12-28T19:41:10.145821-07:00","close_reason":"48/48 tests pass - CachedEmbedder fully implemented","dependencies":[{"issue_id":"Pommel-jkd","depends_on_id":"Pommel-697","type":"blocks","created_at":"2025-12-28T19:20:56.966471-07:00","created_by":"ryan"}]} {"id":"Pommel-jl4","title":"9.1 Write subproject model tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:50:51.326597-07:00","updated_at":"2025-12-29T11:53:21.128509-07:00","closed_at":"2025-12-29T11:53:21.128509-07:00","close_reason":"Subproject model and chunk updates implemented with tests"} {"id":"Pommel-jro","title":"2.1b Implement Ollama Client","description":"TDD: Implement OllamaClient to pass all tests from 2.1a","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:19:48.311775-07:00","updated_at":"2025-12-28T19:29:55.659316-07:00","closed_at":"2025-12-28T19:29:55.659316-07:00","close_reason":"18/18 tests pass - Ollama client fully implemented","dependencies":[{"issue_id":"Pommel-jro","depends_on_id":"Pommel-ct9","type":"blocks","created_at":"2025-12-28T19:20:46.603479-07:00","created_by":"ryan"}]} +{"id":"Pommel-k4iu","title":"2.1: Dynamic Dimensions() method","description":"Update OllamaClient.Dimensions() to use GetDimensionsForModel instead of hardcoded 768. Tests: TestOllamaClient_Dimensions_V2Model, TestOllamaClient_Dimensions_V4Model, TestOllamaClient_Dimensions_UnknownModel","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:52.128685-07:00","created_by":"ryan","updated_at":"2026-01-15T21:51:01.446019-07:00","closed_at":"2026-01-15T21:51:01.446019-07:00","close_reason":"Task 2.1 complete: Dynamic Dimensions() using model registry, TDD, reviewed","dependencies":[{"issue_id":"Pommel-k4iu","depends_on_id":"Pommel-dca0","type":"blocks","created_at":"2026-01-15T21:31:22.558404-07:00","created_by":"ryan"}]} {"id":"Pommel-k5u","title":"Implement pm start --foreground flag","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:27:49.99596-07:00","updated_at":"2025-12-29T08:47:39.640226-07:00","closed_at":"2025-12-29T08:47:39.640226-07:00","close_reason":"All features implemented and tested in v0.2.0","dependencies":[{"issue_id":"Pommel-k5u","depends_on_id":"Pommel-xwq","type":"blocks","created_at":"2025-12-29T08:29:55.151997-07:00","created_by":"ryan"}]} {"id":"Pommel-l4x","title":"3.3b: Implement C# Chunker","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:48:24.952295-07:00","updated_at":"2025-12-28T20:09:22.7291-07:00","closed_at":"2025-12-28T20:09:22.7291-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-l4x","depends_on_id":"Pommel-5g7","type":"blocks","created_at":"2025-12-28T19:50:52.648158-07:00","created_by":"ryan"}]} {"id":"Pommel-l5y","title":"1.1 Project Setup - Go module, directories, entry points","description":"Initialize Go module, create directory structure, .gitignore, and entry points for pm/pommeld binaries","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T18:59:25.308377-07:00","updated_at":"2025-12-28T19:02:10.907428-07:00","closed_at":"2025-12-28T19:02:10.907428-07:00","close_reason":"Go module initialized, directories created, entry points added"} {"id":"Pommel-lgz","title":"Create CONTRIBUTING.md with branching strategy and PR guidelines","description":"Document contribution workflow: feature branches -\u003e dev -\u003e main, code style, PR process, how to report bugs/features","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T08:58:30.498379-07:00","updated_at":"2025-12-29T09:06:06.820325-07:00","closed_at":"2025-12-29T09:06:06.820325-07:00","close_reason":"All tasks completed"} +{"id":"Pommel-m6aq","title":"Phase 3: CLI config model Command","description":"Add 'pm config model' command to view and switch embedding models","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-15T21:29:47.194735-07:00","created_by":"ryan","updated_at":"2026-01-15T22:08:14.115687-07:00","closed_at":"2026-01-15T22:08:14.115687-07:00","close_reason":"Phase 3 complete: pm config model command fully implemented"} {"id":"Pommel-mh6","title":"Phase 3: Search Integration (deduplication, boosting)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T16:52:25.634478-07:00","created_by":"ryan","updated_at":"2026-01-04T17:20:59.471295-07:00","closed_at":"2026-01-04T17:20:59.471295-07:00","close_reason":"Phase 3 complete","dependencies":[{"issue_id":"Pommel-mh6","depends_on_id":"Pommel-1bd","type":"blocks","created_at":"2026-01-04T16:52:43.864648-07:00","created_by":"ryan"}]} {"id":"Pommel-mpp","title":"Phase 21: File Watcher Validation for Windows","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T15:42:37.709571-07:00","updated_at":"2026-01-03T12:34:02.968148-07:00","closed_at":"2026-01-03T12:34:02.968148-07:00","close_reason":"Completed in v0.5.2 release","dependencies":[{"issue_id":"Pommel-mpp","depends_on_id":"Pommel-peq","type":"blocks","created_at":"2025-12-30T15:43:17.099583-07:00","created_by":"ryan"}]} {"id":"Pommel-mqn","title":"Phase 4: Install Scripts","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T10:33:00.621964-07:00","created_by":"ryan","updated_at":"2026-01-04T11:04:06.753985-07:00","closed_at":"2026-01-04T11:04:06.753985-07:00","close_reason":"Phase 4 complete: install scripts with provider selection","dependencies":[{"issue_id":"Pommel-mqn","depends_on_id":"Pommel-emu","type":"blocks","created_at":"2026-01-04T10:33:10.237366-07:00","created_by":"ryan"}]} {"id":"Pommel-n8g","title":"Write tests for --claude flag","description":"TDD: Write tests for pm init --claude flag that adds/updates CLAUDE.md with Pommel usage instructions. Tests should verify: 1) Creates CLAUDE.md if missing 2) Appends to existing CLAUDE.md 3) Doesn't duplicate instructions if already present 4) Instructions include pm search examples with --json flag","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T07:59:00.658984-07:00","updated_at":"2025-12-29T08:06:48.625547-07:00","closed_at":"2025-12-29T08:06:48.625547-07:00","close_reason":"All init flags implemented and tested via TDD"} +{"id":"Pommel-nhog","title":"3.1: Show current model","description":"Implement 'pm config model' with no args to show current model (v2/v4 + full name). Tests: TestConfigModel_ShowCurrent_Default","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:52.79779-07:00","created_by":"ryan","updated_at":"2026-01-15T22:00:33.539016-07:00","closed_at":"2026-01-15T22:00:33.539016-07:00","close_reason":"Task 3.1 complete: pm config model shows current model, TDD, reviewed","dependencies":[{"issue_id":"Pommel-nhog","depends_on_id":"Pommel-p4q4","type":"blocks","created_at":"2026-01-15T21:31:22.590529-07:00","created_by":"ryan"}]} {"id":"Pommel-nox","title":"4.1b: Implement File Watcher","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T20:17:34.158383-07:00","updated_at":"2025-12-28T20:29:48.857495-07:00","closed_at":"2025-12-28T20:29:48.857495-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-nox","depends_on_id":"Pommel-zio","type":"blocks","created_at":"2025-12-28T20:18:55.668155-07:00","created_by":"ryan"}]} {"id":"Pommel-o7w","title":"Phase 9: Schema Changes","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:50:26.047152-07:00","updated_at":"2025-12-29T12:00:17.251724-07:00","closed_at":"2025-12-29T12:00:17.251724-07:00","close_reason":"Phase 9 implementation complete - all tests passing"} {"id":"Pommel-og5","title":"3.5b: Implement JS/TS Chunker","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:48:57.827089-07:00","updated_at":"2025-12-28T20:09:22.730191-07:00","closed_at":"2025-12-28T20:09:22.730191-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-og5","depends_on_id":"Pommel-19i","type":"blocks","created_at":"2025-12-28T19:51:03.217675-07:00","created_by":"ryan"}]} {"id":"Pommel-ojz","title":"10.2 Update Config Schema for Subprojects","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:01:44.759141-07:00","updated_at":"2025-12-29T12:06:43.691725-07:00","closed_at":"2025-12-29T12:06:43.691725-07:00","close_reason":"Phase 10 implementation complete","dependencies":[{"issue_id":"Pommel-ojz","depends_on_id":"Pommel-ayz","type":"blocks","created_at":"2025-12-29T12:02:54.651251-07:00","created_by":"ryan"}]} {"id":"Pommel-ooj","title":"Phase 2: Embedding Pipeline","description":"Integrate with Ollama to generate embeddings and store them in sqlite-vec","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T19:19:17.706124-07:00","updated_at":"2025-12-28T19:41:34.719198-07:00","closed_at":"2025-12-28T19:41:34.719198-07:00","close_reason":"Phase 2 complete - all 76 tests pass, embedder 93.5% coverage","dependencies":[{"issue_id":"Pommel-ooj","depends_on_id":"Pommel-jro","type":"blocks","created_at":"2025-12-28T19:21:17.708907-07:00","created_by":"ryan"},{"issue_id":"Pommel-ooj","depends_on_id":"Pommel-xj4","type":"blocks","created_at":"2025-12-28T19:21:22.8881-07:00","created_by":"ryan"},{"issue_id":"Pommel-ooj","depends_on_id":"Pommel-jkd","type":"blocks","created_at":"2025-12-28T19:21:28.06903-07:00","created_by":"ryan"},{"issue_id":"Pommel-ooj","depends_on_id":"Pommel-1fk","type":"blocks","created_at":"2025-12-28T19:21:33.252581-07:00","created_by":"ryan"}]} {"id":"Pommel-p33","title":"Phase 12: Runtime Detection","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T12:12:16.771463-07:00","updated_at":"2025-12-29T12:14:28.025295-07:00","closed_at":"2025-12-29T12:14:28.025295-07:00","close_reason":"Phase 12 implementation complete"} +{"id":"Pommel-p4q4","title":"1.4: GetShortNameForModel function","description":"Create GetShortNameForModel(fullName) returning short name or empty string. Tests: TestGetShortNameForModel_V2, TestGetShortNameForModel_V4, TestGetShortNameForModel_Unknown","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:57.720775-07:00","created_by":"ryan","updated_at":"2026-01-15T21:47:30.214127-07:00","closed_at":"2026-01-15T21:47:30.214127-07:00","close_reason":"Task 1.4 complete: GetShortNameForModel reverse lookup, TDD, reviewed","dependencies":[{"issue_id":"Pommel-p4q4","depends_on_id":"Pommel-323f","type":"parent-child","created_at":"2026-01-15T21:30:26.310279-07:00","created_by":"ryan"},{"issue_id":"Pommel-p4q4","depends_on_id":"Pommel-5uix","type":"blocks","created_at":"2026-01-15T21:30:33.463678-07:00","created_by":"ryan"}]} {"id":"Pommel-paw","title":"TDD tests for status improvements","description":"Tests for indexer stats, API response, CLI display","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:30:56.698029-07:00","updated_at":"2026-01-03T13:03:21.28277-07:00","closed_at":"2026-01-03T13:03:21.28277-07:00","close_reason":"Existing tests cover the functionality - chunker and status tests all passing","dependencies":[{"issue_id":"Pommel-paw","depends_on_id":"Pommel-c71","type":"blocks","created_at":"2026-01-03T12:32:10.174222-07:00","created_by":"ryan"}]} {"id":"Pommel-peq","title":"Phase 20: Daemon Process Management for Windows","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T15:42:31.583893-07:00","updated_at":"2026-01-03T12:34:02.968653-07:00","closed_at":"2026-01-03T12:34:02.968653-07:00","close_reason":"Completed in v0.5.2 release","dependencies":[{"issue_id":"Pommel-peq","depends_on_id":"Pommel-d8z","type":"blocks","created_at":"2025-12-30T15:43:11.910384-07:00","created_by":"ryan"}]} {"id":"Pommel-q5n","title":"Improve daemon cleanup error handling","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-29T08:29:19.64688-07:00","updated_at":"2025-12-29T08:47:39.642579-07:00","closed_at":"2025-12-29T08:47:39.642579-07:00","close_reason":"All features implemented and tested in v0.2.0"} +{"id":"Pommel-q7p8","title":"Phase 4: Install Script Updates","description":"Add model selection prompt to install.sh and install.ps1 for Ollama provider","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-15T21:29:51.728111-07:00","created_by":"ryan","updated_at":"2026-01-15T22:17:46.854239-07:00","closed_at":"2026-01-15T22:17:46.854239-07:00","close_reason":"Phase 4 complete: Both install scripts updated with model selection"} +{"id":"Pommel-q8ie","title":"4.1: Bash install script","description":"Add select_ollama_model function, SELECTED_MODEL variable, update write_global_config and setup_embedding_model to use selected model","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:52.542396-07:00","created_by":"ryan","updated_at":"2026-01-15T22:13:02.992731-07:00","closed_at":"2026-01-15T22:13:02.992731-07:00","close_reason":"Task 4.1 complete: Bash install script with model selection, reviewed","dependencies":[{"issue_id":"Pommel-q8ie","depends_on_id":"Pommel-5uix","type":"blocks","created_at":"2026-01-15T21:31:22.622517-07:00","created_by":"ryan"}]} {"id":"Pommel-r5x","title":"3.4a: Write Python Chunker tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:48:38.988264-07:00","updated_at":"2025-12-28T20:03:00.862405-07:00","closed_at":"2025-12-28T20:03:00.862405-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-r5x","depends_on_id":"Pommel-c47","type":"blocks","created_at":"2025-12-28T19:50:12.225136-07:00","created_by":"ryan"},{"issue_id":"Pommel-r5x","depends_on_id":"Pommel-bwg","type":"blocks","created_at":"2025-12-28T19:50:17.412439-07:00","created_by":"ryan"}]} {"id":"Pommel-r6f","title":"9.6 Update schema and migrations","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:51:23.829-07:00","updated_at":"2025-12-29T12:00:17.251279-07:00","closed_at":"2025-12-29T12:00:17.251279-07:00","close_reason":"Phase 9 implementation complete - all tests passing","dependencies":[{"issue_id":"Pommel-r6f","depends_on_id":"Pommel-403","type":"blocks","created_at":"2025-12-29T11:52:18.90878-07:00","created_by":"ryan"}]} {"id":"Pommel-s3o","title":"3.4b: Implement Python Chunker","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T19:48:45.099919-07:00","updated_at":"2025-12-28T20:09:22.72977-07:00","closed_at":"2025-12-28T20:09:22.72977-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-s3o","depends_on_id":"Pommel-r5x","type":"blocks","created_at":"2025-12-28T19:50:57.889034-07:00","created_by":"ryan"}]} @@ -157,6 +175,7 @@ {"id":"Pommel-ubg","title":"Phase 26: FTS5 Infrastructure","description":"Implement SQLite FTS5 full-text search infrastructure. Schema, auto-migration, sync on CRUD, query function, bulk population.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T11:27:36.24419-07:00","updated_at":"2026-01-01T11:40:47.88809-07:00","closed_at":"2026-01-01T11:40:47.88809-07:00","close_reason":"Phase 26 FTS5 Infrastructure complete - 47 tests pass"} {"id":"Pommel-v4n","title":"Write Java chunker test file (TDD)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T18:35:47.792433-07:00","updated_at":"2025-12-29T18:39:52.720193-07:00","closed_at":"2025-12-29T18:39:52.720193-07:00","close_reason":"Closed"} {"id":"Pommel-vfp","title":"Update README search result JSON schema","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-29T08:29:26.28377-07:00","updated_at":"2025-12-29T08:47:39.642177-07:00","closed_at":"2025-12-29T08:47:39.642177-07:00","close_reason":"All features implemented and tested in v0.2.0"} +{"id":"Pommel-vud9","title":"1.2: GetModelByFullName function","description":"Create GetModelByFullName(fullName) returning *ModelInfo or nil for unknown. Tests: TestGetModelByFullName_V2, TestGetModelByFullName_V4, TestGetModelByFullName_Unknown","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:55.571361-07:00","created_by":"ryan","updated_at":"2026-01-15T21:41:46.613201-07:00","closed_at":"2026-01-15T21:41:46.613201-07:00","close_reason":"Task 1.2 complete: GetModelByFullName function implemented with TDD, reviewed","dependencies":[{"issue_id":"Pommel-vud9","depends_on_id":"Pommel-323f","type":"parent-child","created_at":"2026-01-15T21:30:24.787663-07:00","created_by":"ryan"},{"issue_id":"Pommel-vud9","depends_on_id":"Pommel-5uix","type":"blocks","created_at":"2026-01-15T21:30:31.504262-07:00","created_by":"ryan"}]} {"id":"Pommel-vwp","title":"Update CLI status display","description":"Show Files: X/Y (Z%), ETA: M:SS remaining during indexing","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:30:50.43037-07:00","updated_at":"2026-01-03T13:02:34.754657-07:00","closed_at":"2026-01-03T13:02:34.754657-07:00","close_reason":"Status improvements complete","dependencies":[{"issue_id":"Pommel-vwp","depends_on_id":"Pommel-0mx","type":"blocks","created_at":"2026-01-03T12:32:03.732678-07:00","created_by":"ryan"}]} {"id":"Pommel-vwx","title":"4.4a: Write REST API tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T20:18:14.226539-07:00","updated_at":"2025-12-28T20:44:29.110281-07:00","closed_at":"2025-12-28T20:44:29.110281-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-vwx","depends_on_id":"Pommel-w73","type":"blocks","created_at":"2025-12-28T20:19:19.910372-07:00","created_by":"ryan"},{"issue_id":"Pommel-vwx","depends_on_id":"Pommel-7hw","type":"blocks","created_at":"2025-12-28T20:19:25.138899-07:00","created_by":"ryan"}]} {"id":"Pommel-w73","title":"4.2b: Implement Indexer","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T20:17:46.658638-07:00","updated_at":"2025-12-28T20:39:50.009114-07:00","closed_at":"2025-12-28T20:39:50.009114-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-w73","depends_on_id":"Pommel-32n","type":"blocks","created_at":"2025-12-28T20:19:12.329129-07:00","created_by":"ryan"}]} @@ -171,6 +190,7 @@ {"id":"Pommel-ybs","title":"9.3 Write subproject DB tests","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T11:51:03.94682-07:00","updated_at":"2025-12-29T12:00:17.249972-07:00","closed_at":"2025-12-29T12:00:17.249972-07:00","close_reason":"Phase 9 implementation complete - all tests passing"} {"id":"Pommel-yfo","title":"Implement ollama_url configuration option","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T08:29:02.803105-07:00","updated_at":"2025-12-29T08:47:39.63531-07:00","closed_at":"2025-12-29T08:47:39.63531-07:00","close_reason":"All features implemented and tested in v0.2.0","dependencies":[{"issue_id":"Pommel-yfo","depends_on_id":"Pommel-tq9","type":"blocks","created_at":"2025-12-29T08:30:21.088969-07:00","created_by":"ryan"}]} {"id":"Pommel-yi4","title":"Update existing tests for Java support","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T18:36:08.701008-07:00","updated_at":"2025-12-29T18:48:44.42413-07:00","closed_at":"2025-12-29T18:48:44.42413-07:00","close_reason":"Removed Java from unsupported language lists in chunker_test.go and treesitter_test.go","dependencies":[{"issue_id":"Pommel-yi4","depends_on_id":"Pommel-wrr","type":"blocks","created_at":"2025-12-29T18:36:42.443434-07:00","created_by":"ryan"}]} +{"id":"Pommel-ysb1","title":"2.2: ContextSize() method","description":"Add OllamaClient.ContextSize() using GetContextSizeForModel. Update embed() to use dynamic context. Remove JinaContextSize constant. Tests: TestOllamaClient_ContextSize_V2Model, TestOllamaClient_ContextSize_V4Model","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-15T21:29:52.935257-07:00","created_by":"ryan","updated_at":"2026-01-15T21:55:36.530823-07:00","closed_at":"2026-01-15T21:55:36.530823-07:00","close_reason":"Task 2.2 complete: ContextSize() with dynamic context, TDD, reviewed","dependencies":[{"issue_id":"Pommel-ysb1","depends_on_id":"Pommel-k4iu","type":"blocks","created_at":"2026-01-15T21:31:05.326118-07:00","created_by":"ryan"}]} {"id":"Pommel-yzc","title":"1.2 Build System - Makefile and linter config","description":"Create Makefile with build/test/lint targets and golangci-lint configuration","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T18:59:31.536731-07:00","updated_at":"2025-12-28T19:02:56.756561-07:00","closed_at":"2025-12-28T19:02:56.756561-07:00","close_reason":"Makefile and golangci-lint config created","dependencies":[{"issue_id":"Pommel-yzc","depends_on_id":"Pommel-l5y","type":"blocks","created_at":"2025-12-28T19:00:09.196544-07:00","created_by":"ryan"}]} {"id":"Pommel-z73","title":"Update chunker to use generated code","description":"Replace hardcoded treesitter.go with generated registry and detection","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T12:30:05.308295-07:00","updated_at":"2026-01-03T12:58:37.475139-07:00","closed_at":"2026-01-03T12:58:37.475139-07:00","close_reason":"Code generator and chunker refactoring complete","dependencies":[{"issue_id":"Pommel-z73","depends_on_id":"Pommel-fd6","type":"blocks","created_at":"2026-01-03T12:31:36.539814-07:00","created_by":"ryan"}]} {"id":"Pommel-zem","title":"5.1b: Implement Search Service","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T21:05:35.384496-07:00","updated_at":"2025-12-28T21:17:59.258296-07:00","closed_at":"2025-12-28T21:17:59.258296-07:00","close_reason":"Closed","dependencies":[{"issue_id":"Pommel-zem","depends_on_id":"Pommel-0ra","type":"blocks","created_at":"2025-12-28T21:07:35.951791-07:00","created_by":"ryan"},{"issue_id":"Pommel-zem","depends_on_id":"Pommel-5gp","type":"blocks","created_at":"2025-12-28T21:07:46.397882-07:00","created_by":"ryan"}]} diff --git a/cmd/pm/main.go b/cmd/pm/main.go index a1dc4b1..35eff5b 100644 --- a/cmd/pm/main.go +++ b/cmd/pm/main.go @@ -9,7 +9,7 @@ import ( // Set at build time via ldflags var ( - version = "0.7.3" + version = "0.8.0" commit = "unknown" date = "unknown" ) diff --git a/cmd/pommeld/main.go b/cmd/pommeld/main.go index d3b75f2..27ebddc 100644 --- a/cmd/pommeld/main.go +++ b/cmd/pommeld/main.go @@ -13,7 +13,7 @@ import ( ) var ( - version = "0.7.3" + version = "0.8.0" commit = "unknown" date = "unknown" ) diff --git a/docs/plans/2026-01-15-jina-v4-embedding-support-design.md b/docs/plans/2026-01-15-jina-v4-embedding-support-design.md new file mode 100644 index 0000000..064f0cb --- /dev/null +++ b/docs/plans/2026-01-15-jina-v4-embedding-support-design.md @@ -0,0 +1,384 @@ +# Jina v4 Embedding Support Design + +**Date:** 2026-01-15 +**Branch:** dev-jina-4 +**Version:** 0.8.0 + +## Overview + +Add support for Jina v4 embeddings as an optional upgrade from the default v2 model, selectable at install time. This provides users with better code search quality while keeping the lightweight v2 as the default for most users. + +## Models + +| Model | Ollama Name | Size | Dimensions | Notes | +|-------|-------------|------|------------|-------| +| v2 (default) | `unclemusclez/jina-embeddings-v2-base-code` | ~300MB | 768 | Lightweight, well-tested | +| v4 (optional) | `sellerscrisp/jina-embeddings-v4-text-code-q4` | ~8GB | 1024 | Better quality, larger | + +## User Experience + +### Install Script Flow + +When Ollama is selected as the provider, prompt for model choice: + +``` +[2/5] Configure embedding provider + + How would you like to generate embeddings? + + 1) Local Ollama - Free, runs on this machine + 2) Remote Ollama - Free, connect to Ollama on another machine + 3) OpenAI API - Paid, no local setup required + 4) Voyage AI - Paid, optimized for code search + + Choice [1]: 1 + + Which embedding model? + + 1) Standard - Jina v2 Code (~300MB, faster, good quality) (Recommended) + 2) Maximum - Jina v4 Code (~8GB, slower, best quality) + + Choice [1]: _ +``` + +### Per-Project Override + +```bash +pm config model v4 # Switch this project to v4 (triggers reindex prompt) +pm config model v2 # Switch back to v2 +``` + +## Configuration Storage + +### Global Config (`~/.config/pommel/config.yaml`) + +```yaml +embedding: + provider: ollama + ollama: + url: "http://localhost:11434" + model: "unclemusclez/jina-embeddings-v2-base-code" +``` + +### Per-Project Override (`.pommel/config.yaml`) + +```yaml +embedding: + ollama: + model: "sellerscrisp/jina-embeddings-v4-text-code-q4" +``` + +### Model Metadata (Hardcoded in Go) + +```go +var EmbeddingModels = map[string]ModelInfo{ + "v2": { + Name: "unclemusclez/jina-embeddings-v2-base-code", + Dimensions: 768, + Size: "~300MB", + }, + "v4": { + Name: "sellerscrisp/jina-embeddings-v4-text-code-q4", + Dimensions: 1024, + Size: "~8GB", + }, +} +``` + +## Code Changes + +### Files to Modify + +1. **`scripts/install.sh`** (and `install.ps1`) + - Add model selection prompt after provider selection + - Store model choice in global config + - Pull the selected model + +2. **`internal/embedder/ollama.go`** + - Add `ModelInfo` struct and model registry + - Update `Dimensions()` to return based on configured model + - Remove hardcoded `JinaContextSize` constant, make it model-aware + +3. **`internal/cli/config.go`** + - Add `pm config model [v2|v4]` subcommand + - Warn user that changing model requires reindex + - Update config file with new model + +### No Changes Needed + +- `internal/config/config.go` - Already supports `embedding.ollama.model` +- `internal/db/vectors.go` - Already supports dynamic dimensions via `db.Open(path, dimensions)` +- Daemon already reads model from config + +## Model Switching Behavior + +### When user runs `pm config model v4` + +1. Check if daemon is running - if yes, prompt to stop it first +2. Check current model in config vs requested model +3. If different: + - Warn: "Switching models requires a full reindex. The existing index will be deleted." + - Prompt: "Continue? (y/N)" + - If yes: Update config, delete `.pommel/pommel.db` + - Print: "Model changed to v4. Run `pm start` to reindex with the new model." + +### When daemon starts with mismatched dimensions + +If the database exists but has different dimensions than the configured model: +- Log error: "Database dimensions (768) don't match configured model (1024). Run `pm reindex --force` or `pm config model v2` to fix." +- Exit with error (don't silently corrupt data) + +### Fresh project (`pm init`) + +Uses whatever model is in global config. No special handling needed. + +## Edge Cases + +1. **Remote Ollama with model choice:** Same prompt, but warn that the model must also be available on the remote server + +2. **Upgrade from v0.7.3:** Existing installs keep their current model (v2). No automatic migration. + +3. **Unknown model in config:** If user manually sets a model not in our registry, fall back to querying Ollama for dimensions or default to 768 + +4. **Model pull fails:** Same behavior as today - warn and continue, user can pull manually + +## Testing Strategy + +Strict TDD: All tests must be written and fail before implementation code is written. + +### Test Files to Create/Modify + +| File | Purpose | +|------|---------| +| `internal/embedder/models_test.go` | Model registry tests | +| `internal/embedder/ollama_test.go` | Ollama client dimension tests | +| `internal/cli/config_model_test.go` | `pm config model` command tests | +| `internal/daemon/daemon_test.go` | Dimension mismatch detection tests | +| `internal/config/config_test.go` | Config loading/override tests | + +--- + +### 1. Model Registry Tests (`internal/embedder/models_test.go`) + +#### Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestGetModelInfo_V2` | Look up v2 model by short name | Returns correct name, 768 dims | +| `TestGetModelInfo_V4` | Look up v4 model by short name | Returns correct name, 1024 dims | +| `TestGetModelInfo_ByFullName` | Look up by full Ollama model name | Returns matching ModelInfo | +| `TestGetDimensions_V2` | Get dimensions for v2 model | Returns 768 | +| `TestGetDimensions_V4` | Get dimensions for v4 model | Returns 1024 | + +#### Failure Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestGetModelInfo_UnknownShortName` | Look up "v5" (doesn't exist) | Returns error | +| `TestGetModelInfo_EmptyName` | Look up empty string | Returns error | + +#### Edge Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `TestGetModelInfo_UnknownFullName` | Look up unregistered Ollama model name | Returns nil, no error (unknown model) | +| `TestGetDimensions_UnknownModel_DefaultsTo768` | Get dims for unknown model | Returns 768 (safe default) | +| `TestIsKnownModel_V2` | Check if v2 full name is known | Returns true | +| `TestIsKnownModel_Unknown` | Check if random model is known | Returns false | + +--- + +### 2. Ollama Client Tests (`internal/embedder/ollama_test.go`) + +#### Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestOllamaClient_Dimensions_V2Model` | Client configured with v2 model | `Dimensions()` returns 768 | +| `TestOllamaClient_Dimensions_V4Model` | Client configured with v4 model | `Dimensions()` returns 1024 | +| `TestOllamaClient_ContextSize_V2Model` | Client configured with v2 model | Context size is 8192 | +| `TestOllamaClient_ContextSize_V4Model` | Client configured with v4 model | Context size is 32768 | + +#### Edge Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `TestOllamaClient_Dimensions_UnknownModel` | Client with unregistered model | Returns 768 (safe default) | +| `TestOllamaClient_Dimensions_EmptyModel` | Client with empty model string | Returns 768 (default) | + +--- + +### 3. Config Model Command Tests (`internal/cli/config_model_test.go`) + +#### Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfigModel_ShowCurrent` | Run `pm config model` with no args | Prints current model (v2 or v4) | +| `TestConfigModel_SetV4_Confirmed` | Set to v4, user confirms | Config updated, db deleted, success message | +| `TestConfigModel_SetV2_Confirmed` | Set to v2, user confirms | Config updated, db deleted, success message | +| `TestConfigModel_SameModel` | Set to already-configured model | No-op, prints "already using v2" | + +#### Success Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfigModel_SetV4_NoExistingDB` | Set to v4, no db exists | Config updated, no deletion needed | +| `TestConfigModel_SetV4_ProjectConfigCreated` | Set in project dir | `.pommel/config.yaml` created/updated | +| `TestConfigModel_OutputIncludesReindexInstructions` | After successful switch | Output includes "Run `pm start` to reindex" | + +#### Failure Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfigModel_SetV4_Declined` | Set to v4, user says no | Config unchanged, db unchanged | +| `TestConfigModel_InvalidModel` | Run `pm config model v5` | Error: "unknown model 'v5', use v2 or v4" | +| `TestConfigModel_DaemonRunning` | Switch while daemon runs | Error: "stop daemon first with `pm stop`" | + +#### Error Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfigModel_ConfigWriteError` | Config file not writable | Error with helpful message | +| `TestConfigModel_DBDeleteError` | DB file locked/not deletable | Error with helpful message | +| `TestConfigModel_NoProjectRoot` | Run outside any project | Uses global config or errors appropriately | + +#### Edge Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfigModel_CaseInsensitive` | Run `pm config model V4` | Works same as `v4` | +| `TestConfigModel_FullModelName` | Run `pm config model unclemusclez/...` | Recognizes and maps to v2 | +| `TestConfigModel_ProjectOverridesGlobal` | Global=v2, project=v4 | Shows v4 as current | + +--- + +### 4. Daemon Dimension Mismatch Tests (`internal/daemon/daemon_test.go`) + +#### Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestDaemon_Start_V2Config_V2DB` | Config=v2, DB has 768 dims | Starts successfully | +| `TestDaemon_Start_V4Config_V4DB` | Config=v4, DB has 1024 dims | Starts successfully | +| `TestDaemon_Start_V2Config_NoDB` | Config=v2, no existing DB | Creates DB with 768 dims, starts | +| `TestDaemon_Start_V4Config_NoDB` | Config=v4, no existing DB | Creates DB with 1024 dims, starts | + +#### Failure Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestDaemon_Start_V4Config_V2DB` | Config=v4, DB has 768 dims | Error: dimension mismatch | +| `TestDaemon_Start_V2Config_V4DB` | Config=v2, DB has 1024 dims | Error: dimension mismatch | + +#### Error Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestDaemon_DimensionMismatch_ErrorMessage` | Mismatch detected | Error includes both dimensions and fix instructions | +| `TestDaemon_DimensionMismatch_ExitCode` | Mismatch detected | Non-zero exit code | + +#### Edge Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `TestDaemon_Start_UnknownModel_ExistingDB` | Unknown model, DB exists | Uses DB's dimensions, warns | +| `TestDaemon_Start_CorruptedDB` | DB exists but unreadable | Error with helpful message | + +--- + +### 5. Config Loading Tests (`internal/config/config_test.go`) + +#### Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfig_LoadGlobalModel` | Global config has model set | Returns correct model name | +| `TestConfig_LoadProjectModel` | Project config has model set | Returns project model | +| `TestConfig_ProjectOverridesGlobal` | Both set, different values | Project model wins | +| `TestConfig_DefaultModel_NoConfig` | No config files exist | Returns v2 model name | + +#### Success Scenarios + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfig_GlobalOnly_NoProjectConfig` | Global set, no project config | Returns global model | +| `TestConfig_PartialProjectConfig` | Project config exists but no model | Falls back to global model | + +#### Edge Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `TestConfig_EmptyModelString` | Model set to empty string | Falls back to default v2 | +| `TestConfig_WhitespaceModelString` | Model set to whitespace | Falls back to default v2 | +| `TestConfig_OllamaRemote_ModelConfig` | Remote Ollama with model | Model config still applies | + +--- + +### 6. Integration Tests + +#### End-to-End Happy Path + +| Test | Description | Expected | +|------|-------------|----------| +| `TestE2E_FreshInstall_DefaultV2` | New install, accept defaults | v2 model configured, 768-dim DB | +| `TestE2E_FreshInstall_SelectV4` | New install, select v4 | v4 model configured, 1024-dim DB | +| `TestE2E_SwitchV2ToV4_Reindex` | Start v2, switch to v4, reindex | Search works with new model | +| `TestE2E_ProjectOverride` | Global v2, project v4 | Project uses v4, other projects use v2 | + +#### End-to-End Failure Recovery + +| Test | Description | Expected | +|------|-------------|----------| +| `TestE2E_MismatchDetection_Recovery` | Create mismatch, run daemon | Clear error, user can fix with suggested command | +| `TestE2E_SwitchModel_CancelMidway` | Start switch, cancel | Original config/db preserved | + +--- + +### Test Utilities to Create + +```go +// internal/testutil/models.go + +// CreateTestDBWithDimensions creates a test database with specified dimensions +func CreateTestDBWithDimensions(t *testing.T, path string, dims int) *db.DB + +// CreateTestConfig creates a test config with specified model +func CreateTestConfig(t *testing.T, dir string, model string) *config.Config + +// MockUserInput simulates user input for interactive prompts +func MockUserInput(t *testing.T, inputs ...string) func() +``` + +--- + +### TDD Workflow + +For each component: + +1. **Write failing test** - Test must fail with clear "not implemented" or wrong value +2. **Verify failure reason** - Confirm test fails for the right reason +3. **Write minimal code** - Just enough to pass the test +4. **Verify pass** - All tests green +5. **Refactor** - Clean up while keeping tests green +6. **Repeat** - Next test case + +### Test Execution Order + +1. Model registry tests (foundation) +2. Config loading tests (depends on registry) +3. Ollama client dimension tests (depends on registry) +4. Config model command tests (depends on config loading) +5. Daemon mismatch tests (depends on all above) +6. Integration tests (full system) + +## Summary + +| Aspect | Decision | +|--------|----------| +| Default model | v2 (~300MB, 768 dims) | +| Optional model | v4 (~8GB, 1024 dims) | +| Choice timing | Install script, after Ollama provider selection | +| Storage | Global default + per-project override | +| Model switching | Requires explicit reindex, warns user | +| Dimension mismatch | Hard error, don't corrupt data | diff --git a/docs/plans/2026-01-15-jina-v4-implementation.md b/docs/plans/2026-01-15-jina-v4-implementation.md new file mode 100644 index 0000000..471ae58 --- /dev/null +++ b/docs/plans/2026-01-15-jina-v4-implementation.md @@ -0,0 +1,1320 @@ +# Jina v4 Embedding Support - Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add optional Jina v4 embeddings (1024 dims) alongside default v2 (768 dims), selectable at install time. + +**Architecture:** Model registry maps short names (v2/v4) to full Ollama model names and dimensions. Install script prompts for model choice. `pm config model` command allows switching with reindex. + +**Tech Stack:** Go, Cobra CLI, testify, bash/PowerShell install scripts + +--- + +## Task 1: Create Model Registry + +**Files:** +- Create: `internal/embedder/models.go` +- Create: `internal/embedder/models_test.go` + +### Step 1.1: Write failing test for GetModelInfo_V2 + +```go +// internal/embedder/models_test.go +package embedder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetModelInfo_V2(t *testing.T) { + info, err := GetModelInfo("v2") + require.NoError(t, err) + assert.Equal(t, "unclemusclez/jina-embeddings-v2-base-code", info.Name) + assert.Equal(t, 768, info.Dimensions) + assert.Equal(t, 8192, info.ContextSize) +} +``` + +### Step 1.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_V2 -v +``` + +Expected: FAIL - `undefined: GetModelInfo` + +### Step 1.3: Write minimal implementation + +```go +// internal/embedder/models.go +package embedder + +import ( + "fmt" + "strings" +) + +// ModelInfo contains metadata about an embedding model. +type ModelInfo struct { + Name string // Full Ollama model name + Dimensions int // Embedding vector dimensions + ContextSize int // Maximum context window in tokens + Size string // Human-readable size (e.g., "~300MB") +} + +// EmbeddingModels maps short names to model info. +var EmbeddingModels = map[string]ModelInfo{ + "v2": { + Name: "unclemusclez/jina-embeddings-v2-base-code", + Dimensions: 768, + ContextSize: 8192, + Size: "~300MB", + }, + "v4": { + Name: "sellerscrisp/jina-embeddings-v4-text-code-q4", + Dimensions: 1024, + ContextSize: 32768, + Size: "~8GB", + }, +} + +// DefaultModel is the short name of the default embedding model. +const DefaultModel = "v2" + +// GetModelInfo returns model info by short name (v2, v4). +func GetModelInfo(shortName string) (*ModelInfo, error) { + shortName = strings.ToLower(strings.TrimSpace(shortName)) + if shortName == "" { + return nil, fmt.Errorf("model name cannot be empty") + } + info, ok := EmbeddingModels[shortName] + if !ok { + return nil, fmt.Errorf("unknown model '%s', use v2 or v4", shortName) + } + return &info, nil +} +``` + +### Step 1.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_V2 -v +``` + +Expected: PASS + +### Step 1.5: Write failing test for GetModelInfo_V4 + +```go +func TestGetModelInfo_V4(t *testing.T) { + info, err := GetModelInfo("v4") + require.NoError(t, err) + assert.Equal(t, "sellerscrisp/jina-embeddings-v4-text-code-q4", info.Name) + assert.Equal(t, 1024, info.Dimensions) + assert.Equal(t, 32768, info.ContextSize) +} +``` + +### Step 1.6: Run test to verify it passes (already implemented) + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_V4 -v +``` + +Expected: PASS + +### Step 1.7: Write failing test for unknown model + +```go +func TestGetModelInfo_UnknownShortName(t *testing.T) { + _, err := GetModelInfo("v5") + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown model 'v5'") +} +``` + +### Step 1.8: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_UnknownShortName -v +``` + +Expected: PASS + +### Step 1.9: Write failing test for empty name + +```go +func TestGetModelInfo_EmptyName(t *testing.T) { + _, err := GetModelInfo("") + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot be empty") +} +``` + +### Step 1.10: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_EmptyName -v +``` + +Expected: PASS + +### Step 1.11: Write failing test for case insensitivity + +```go +func TestGetModelInfo_CaseInsensitive(t *testing.T) { + info, err := GetModelInfo("V4") + require.NoError(t, err) + assert.Equal(t, 1024, info.Dimensions) +} +``` + +### Step 1.12: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelInfo_CaseInsensitive -v +``` + +Expected: PASS + +### Step 1.13: Commit + +```bash +git add internal/embedder/models.go internal/embedder/models_test.go +git commit -m "feat(embedder): add model registry for v2/v4 embedding models" +``` + +--- + +## Task 2: Add GetModelByFullName Function + +**Files:** +- Modify: `internal/embedder/models.go` +- Modify: `internal/embedder/models_test.go` + +### Step 2.1: Write failing test + +```go +func TestGetModelByFullName_V2(t *testing.T) { + info := GetModelByFullName("unclemusclez/jina-embeddings-v2-base-code") + require.NotNil(t, info) + assert.Equal(t, 768, info.Dimensions) +} + +func TestGetModelByFullName_V4(t *testing.T) { + info := GetModelByFullName("sellerscrisp/jina-embeddings-v4-text-code-q4") + require.NotNil(t, info) + assert.Equal(t, 1024, info.Dimensions) +} + +func TestGetModelByFullName_Unknown(t *testing.T) { + info := GetModelByFullName("some-random-model") + assert.Nil(t, info) +} +``` + +### Step 2.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelByFullName -v +``` + +Expected: FAIL - `undefined: GetModelByFullName` + +### Step 2.3: Write minimal implementation + +Add to `internal/embedder/models.go`: + +```go +// GetModelByFullName returns model info by full Ollama model name. +// Returns nil if the model is not in our registry (unknown model). +func GetModelByFullName(fullName string) *ModelInfo { + fullName = strings.TrimSpace(fullName) + for _, info := range EmbeddingModels { + if info.Name == fullName { + return &info + } + } + return nil +} +``` + +### Step 2.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetModelByFullName -v +``` + +Expected: PASS + +### Step 2.5: Commit + +```bash +git add internal/embedder/models.go internal/embedder/models_test.go +git commit -m "feat(embedder): add GetModelByFullName lookup function" +``` + +--- + +## Task 3: Add GetDimensionsForModel Function + +**Files:** +- Modify: `internal/embedder/models.go` +- Modify: `internal/embedder/models_test.go` + +### Step 3.1: Write failing tests + +```go +func TestGetDimensionsForModel_V2(t *testing.T) { + dims := GetDimensionsForModel("unclemusclez/jina-embeddings-v2-base-code") + assert.Equal(t, 768, dims) +} + +func TestGetDimensionsForModel_V4(t *testing.T) { + dims := GetDimensionsForModel("sellerscrisp/jina-embeddings-v4-text-code-q4") + assert.Equal(t, 1024, dims) +} + +func TestGetDimensionsForModel_Unknown_DefaultsTo768(t *testing.T) { + dims := GetDimensionsForModel("some-unknown-model") + assert.Equal(t, 768, dims, "unknown models should default to 768") +} + +func TestGetDimensionsForModel_Empty_DefaultsTo768(t *testing.T) { + dims := GetDimensionsForModel("") + assert.Equal(t, 768, dims, "empty model should default to 768") +} +``` + +### Step 3.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetDimensionsForModel -v +``` + +Expected: FAIL - `undefined: GetDimensionsForModel` + +### Step 3.3: Write minimal implementation + +Add to `internal/embedder/models.go`: + +```go +// DefaultDimensions is the fallback for unknown models. +const DefaultDimensions = 768 + +// GetDimensionsForModel returns dimensions for a model by full name. +// Returns DefaultDimensions (768) for unknown models. +func GetDimensionsForModel(fullName string) int { + info := GetModelByFullName(fullName) + if info == nil { + return DefaultDimensions + } + return info.Dimensions +} +``` + +### Step 3.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetDimensionsForModel -v +``` + +Expected: PASS + +### Step 3.5: Commit + +```bash +git add internal/embedder/models.go internal/embedder/models_test.go +git commit -m "feat(embedder): add GetDimensionsForModel with safe defaults" +``` + +--- + +## Task 4: Update OllamaClient to Use Model Registry + +**Files:** +- Modify: `internal/embedder/ollama.go` +- Modify: `internal/embedder/ollama_test.go` (create if needed) + +### Step 4.1: Write failing test for dynamic dimensions + +```go +// internal/embedder/ollama_test.go +package embedder + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOllamaClient_Dimensions_V2Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "unclemusclez/jina-embeddings-v2-base-code", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 768, client.Dimensions()) +} + +func TestOllamaClient_Dimensions_V4Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "sellerscrisp/jina-embeddings-v4-text-code-q4", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 1024, client.Dimensions()) +} + +func TestOllamaClient_Dimensions_UnknownModel(t *testing.T) { + cfg := OllamaConfig{ + Model: "some-random-model", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 768, client.Dimensions(), "unknown models default to 768") +} +``` + +### Step 4.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestOllamaClient_Dimensions -v +``` + +Expected: FAIL - V4 test returns 768 instead of 1024 + +### Step 4.3: Update Dimensions() method + +Modify `internal/embedder/ollama.go`: + +```go +// Dimensions returns the embedding dimension size based on the configured model. +func (c *OllamaClient) Dimensions() int { + return GetDimensionsForModel(c.model) +} +``` + +### Step 4.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestOllamaClient_Dimensions -v +``` + +Expected: PASS + +### Step 4.5: Commit + +```bash +git add internal/embedder/ollama.go internal/embedder/ollama_test.go +git commit -m "feat(embedder): dynamic dimensions based on configured model" +``` + +--- + +## Task 5: Add ContextSize Method to OllamaClient + +**Files:** +- Modify: `internal/embedder/ollama.go` +- Modify: `internal/embedder/ollama_test.go` + +### Step 5.1: Write failing tests + +```go +func TestOllamaClient_ContextSize_V2Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "unclemusclez/jina-embeddings-v2-base-code", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 8192, client.ContextSize()) +} + +func TestOllamaClient_ContextSize_V4Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "sellerscrisp/jina-embeddings-v4-text-code-q4", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 32768, client.ContextSize()) +} +``` + +### Step 5.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestOllamaClient_ContextSize -v +``` + +Expected: FAIL - `undefined: client.ContextSize` + +### Step 5.3: Add GetContextSizeForModel and ContextSize method + +Add to `internal/embedder/models.go`: + +```go +// DefaultContextSize is the fallback for unknown models. +const DefaultContextSize = 8192 + +// GetContextSizeForModel returns context size for a model by full name. +// Returns DefaultContextSize (8192) for unknown models. +func GetContextSizeForModel(fullName string) int { + info := GetModelByFullName(fullName) + if info == nil { + return DefaultContextSize + } + return info.ContextSize +} +``` + +Add to `internal/embedder/ollama.go`: + +```go +// ContextSize returns the context window size based on the configured model. +func (c *OllamaClient) ContextSize() int { + return GetContextSizeForModel(c.model) +} +``` + +### Step 5.4: Update embed() to use dynamic context size + +Modify the embed() method in `internal/embedder/ollama.go`: + +```go +func (c *OllamaClient) embed(ctx context.Context, input any) ([][]float32, error) { + reqBody := ollamaEmbedRequest{ + Model: c.model, + Input: input, + Options: map[string]interface{}{ + "num_ctx": c.ContextSize(), + }, + } + // ... rest unchanged +} +``` + +### Step 5.5: Remove hardcoded JinaContextSize constant + +Delete this line from `internal/embedder/ollama.go`: + +```go +// DELETE THIS: +// const JinaContextSize = 8192 +``` + +### Step 5.6: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestOllamaClient_ContextSize -v +``` + +Expected: PASS + +### Step 5.7: Run all embedder tests + +```bash +go test -tags fts5 ./internal/embedder/... -v +``` + +Expected: All PASS + +### Step 5.8: Commit + +```bash +git add internal/embedder/models.go internal/embedder/ollama.go internal/embedder/ollama_test.go +git commit -m "feat(embedder): dynamic context size based on model" +``` + +--- + +## Task 6: Add GetShortNameForModel Function + +**Files:** +- Modify: `internal/embedder/models.go` +- Modify: `internal/embedder/models_test.go` + +### Step 6.1: Write failing tests + +```go +func TestGetShortNameForModel_V2(t *testing.T) { + name := GetShortNameForModel("unclemusclez/jina-embeddings-v2-base-code") + assert.Equal(t, "v2", name) +} + +func TestGetShortNameForModel_V4(t *testing.T) { + name := GetShortNameForModel("sellerscrisp/jina-embeddings-v4-text-code-q4") + assert.Equal(t, "v4", name) +} + +func TestGetShortNameForModel_Unknown(t *testing.T) { + name := GetShortNameForModel("some-unknown-model") + assert.Equal(t, "", name) +} +``` + +### Step 6.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetShortNameForModel -v +``` + +Expected: FAIL - `undefined: GetShortNameForModel` + +### Step 6.3: Write minimal implementation + +Add to `internal/embedder/models.go`: + +```go +// GetShortNameForModel returns the short name (v2, v4) for a full model name. +// Returns empty string if model is not in registry. +func GetShortNameForModel(fullName string) string { + fullName = strings.TrimSpace(fullName) + for shortName, info := range EmbeddingModels { + if info.Name == fullName { + return shortName + } + } + return "" +} +``` + +### Step 6.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/embedder/... -run TestGetShortNameForModel -v +``` + +Expected: PASS + +### Step 6.5: Commit + +```bash +git add internal/embedder/models.go internal/embedder/models_test.go +git commit -m "feat(embedder): add GetShortNameForModel reverse lookup" +``` + +--- + +## Task 7: Add pm config model Command - Show Current + +**Files:** +- Create: `internal/cli/config_model.go` +- Create: `internal/cli/config_model_test.go` + +### Step 7.1: Write failing test + +```go +// internal/cli/config_model_test.go +package cli + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigModel_ShowCurrent_Default(t *testing.T) { + // Setup: create temp project with default config + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + projectRoot = projectDir + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "v2") + assert.Contains(t, stdout.String(), "unclemusclez/jina-embeddings-v2-base-code") +} +``` + +### Step 7.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_ShowCurrent -v +``` + +Expected: FAIL - `undefined: newConfigModelCmd` + +### Step 7.3: Write minimal implementation + +```go +// internal/cli/config_model.go +package cli + +import ( + "fmt" + + "github.com/pommel-dev/pommel/internal/config" + "github.com/pommel-dev/pommel/internal/embedder" + "github.com/spf13/cobra" +) + +func init() { + configCmd.AddCommand(newConfigModelCmd()) +} + +func newConfigModelCmd() *cobra.Command { + return &cobra.Command{ + Use: "model [v2|v4]", + Short: "View or change the embedding model", + Long: `View or change the embedding model used for code search. + +Without arguments, shows the current model. +With an argument, switches to the specified model (requires reindex). + +Available models: + v2 - Jina v2 Code (~300MB, 768 dims) - lightweight, fast + v4 - Jina v4 Code (~8GB, 1024 dims) - best quality, larger`, + RunE: runConfigModel, + } +} + +func runConfigModel(cmd *cobra.Command, args []string) error { + loader := config.NewLoader(projectRoot) + cfg, err := loader.Load() + if err != nil { + return ErrConfigInvalid(err) + } + + // No args - show current model + if len(args) == 0 { + return showCurrentModel(cmd, cfg) + } + + // TODO: implement model switching + return fmt.Errorf("model switching not yet implemented") +} + +func showCurrentModel(cmd *cobra.Command, cfg *config.Config) error { + modelName := cfg.Embedding.Ollama.Model + if modelName == "" { + modelName = embedder.EmbeddingModels[embedder.DefaultModel].Name + } + + shortName := embedder.GetShortNameForModel(modelName) + if shortName == "" { + shortName = "custom" + } + + fmt.Fprintf(cmd.OutOrStdout(), "Current model: %s (%s)\n", shortName, modelName) + return nil +} +``` + +### Step 7.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_ShowCurrent -v +``` + +Expected: PASS + +### Step 7.5: Commit + +```bash +git add internal/cli/config_model.go internal/cli/config_model_test.go +git commit -m "feat(cli): add 'pm config model' to show current model" +``` + +--- + +## Task 8: Add Model Switching with Confirmation + +**Files:** +- Modify: `internal/cli/config_model.go` +- Modify: `internal/cli/config_model_test.go` + +### Step 8.1: Write failing test for switching + +```go +func TestConfigModel_SetV4_NoExistingDB(t *testing.T) { + // Setup: create temp project with v2 config, no database + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + configPath := filepath.Join(pommelDir, "config.yaml") + require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644)) + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"v4"}) + projectRoot = projectDir + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "v4") + assert.Contains(t, stdout.String(), "pm start") + + // Verify config was updated + content, err := os.ReadFile(configPath) + require.NoError(t, err) + assert.Contains(t, string(content), "sellerscrisp/jina-embeddings-v4-text-code-q4") +} +``` + +### Step 8.2: Run test to verify it fails + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_SetV4 -v +``` + +Expected: FAIL - returns "model switching not yet implemented" + +### Step 8.3: Implement model switching + +Update `internal/cli/config_model.go`: + +```go +func runConfigModel(cmd *cobra.Command, args []string) error { + loader := config.NewLoader(projectRoot) + cfg, err := loader.Load() + if err != nil { + return ErrConfigInvalid(err) + } + + // No args - show current model + if len(args) == 0 { + return showCurrentModel(cmd, cfg) + } + + // Switch to requested model + return switchModel(cmd, loader, cfg, args[0]) +} + +func switchModel(cmd *cobra.Command, loader *config.Loader, cfg *config.Config, target string) error { + // Validate target model + targetInfo, err := embedder.GetModelInfo(target) + if err != nil { + return NewCLIError( + err.Error(), + "Available models: v2 (lightweight), v4 (best quality)") + } + + // Check if already using this model + currentModel := cfg.Embedding.Ollama.Model + if currentModel == targetInfo.Name { + shortName := embedder.GetShortNameForModel(currentModel) + fmt.Fprintf(cmd.OutOrStdout(), "Already using model %s\n", shortName) + return nil + } + + // Check for existing database + dbPath := filepath.Join(projectRoot, ".pommel", "pommel.db") + dbExists := false + if _, err := os.Stat(dbPath); err == nil { + dbExists = true + } + + // Delete database if it exists + if dbExists { + if err := os.Remove(dbPath); err != nil { + return WrapError(err, + "Failed to delete existing database", + "Check file permissions or delete .pommel/pommel.db manually") + } + } + + // Update config + cfg.Embedding.Ollama.Model = targetInfo.Name + if err := loader.Save(cfg); err != nil { + return WrapError(err, + "Failed to save configuration", + "Check write permissions for the .pommel directory") + } + + shortName := embedder.GetShortNameForModel(targetInfo.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Switched to model %s (%s)\n", shortName, targetInfo.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Run 'pm start' to reindex with the new model.\n") + return nil +} +``` + +Add import at top: + +```go +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pommel-dev/pommel/internal/config" + "github.com/pommel-dev/pommel/internal/embedder" + "github.com/spf13/cobra" +) +``` + +### Step 8.4: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_SetV4 -v +``` + +Expected: PASS + +### Step 8.5: Commit + +```bash +git add internal/cli/config_model.go internal/cli/config_model_test.go +git commit -m "feat(cli): implement model switching in 'pm config model'" +``` + +--- + +## Task 9: Add Invalid Model Test + +**Files:** +- Modify: `internal/cli/config_model_test.go` + +### Step 9.1: Write failing test + +```go +func TestConfigModel_InvalidModel(t *testing.T) { + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + var stderr bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"v5"}) + projectRoot = projectDir + + err := cmd.Execute() + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown model 'v5'") +} +``` + +### Step 9.2: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_InvalidModel -v +``` + +Expected: PASS (already implemented in switchModel) + +### Step 9.3: Commit + +```bash +git add internal/cli/config_model_test.go +git commit -m "test(cli): add invalid model test for pm config model" +``` + +--- + +## Task 10: Add Same Model No-Op Test + +**Files:** +- Modify: `internal/cli/config_model_test.go` + +### Step 10.1: Write test + +```go +func TestConfigModel_SameModel(t *testing.T) { + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"v2"}) + projectRoot = projectDir + + err := cmd.Execute() + + require.NoError(t, err) + assert.Contains(t, stdout.String(), "Already using") +} +``` + +### Step 10.2: Run test to verify it passes + +```bash +go test -tags fts5 ./internal/cli/... -run TestConfigModel_SameModel -v +``` + +Expected: PASS + +### Step 10.3: Commit + +```bash +git add internal/cli/config_model_test.go +git commit -m "test(cli): add same-model no-op test" +``` + +--- + +## Task 11: Update Install Script - Model Selection + +**Files:** +- Modify: `scripts/install.sh` + +### Step 11.1: Add model selection variable + +Add after line 43 (OLLAMA_INSTALLED): + +```bash +SELECTED_MODEL="v2" # Default to v2 +``` + +### Step 11.2: Add select_ollama_model function + +Add after `setup_voyage()` function: + +```bash +select_ollama_model() { + echo "" + echo " Which embedding model?" + echo "" + echo " 1) Standard - Jina v2 Code (~300MB, faster, good quality) (Recommended)" + echo " 2) Maximum - Jina v4 Code (~8GB, slower, best quality)" + echo "" + read -p " Choice [1]: " model_choice < /dev/tty + model_choice=${model_choice:-1} + + case $model_choice in + 1) + SELECTED_MODEL="v2" + success "Selected: Jina v2 Code (Standard)" + ;; + 2) + SELECTED_MODEL="v4" + success "Selected: Jina v4 Code (Maximum quality)" + ;; + *) + warn "Invalid choice. Using Standard (v2)." + SELECTED_MODEL="v2" + ;; + esac +} +``` + +### Step 11.3: Call select_ollama_model after Ollama setup + +Modify `setup_local_ollama()` to call model selection: + +```bash +setup_local_ollama() { + SELECTED_PROVIDER="ollama" + success "Selected: Local Ollama" + + # Check for Ollama + if ! command -v ollama &> /dev/null; then + warn "Ollama not found on this machine." + echo "" + echo " Install Ollama from: https://ollama.ai/download" + echo "" + read -p " Continue anyway? (y/N) " -n 1 -r < /dev/tty + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + select_provider + return + fi + OLLAMA_INSTALLED=false + else + success "Ollama found" + OLLAMA_INSTALLED=true + fi + + # Select model + select_ollama_model +} +``` + +Also modify `setup_remote_ollama()`: + +```bash +setup_remote_ollama() { + SELECTED_PROVIDER="ollama-remote" + echo "" + read -p " Enter Ollama server URL (e.g., http://192.168.1.100:11434): " url < /dev/tty + + if [[ -z "$url" ]]; then + warn "URL is required for remote Ollama" + setup_remote_ollama + return + fi + + OLLAMA_REMOTE_URL="$url" + success "Selected: Remote Ollama at $url" + + # Select model + select_ollama_model + warn "Make sure the selected model is available on the remote server" +} +``` + +### Step 11.4: Update write_global_config to use selected model + +Modify the ollama and ollama-remote cases in `write_global_config()`: + +```bash + case $SELECTED_PROVIDER in + ollama) + local model_name + if [[ "$SELECTED_MODEL" == "v4" ]]; then + model_name="sellerscrisp/jina-embeddings-v4-text-code-q4" + else + model_name="unclemusclez/jina-embeddings-v2-base-code" + fi + cat >> "$config_file" << EOF + ollama: + url: "http://localhost:11434" + model: "$model_name" +EOF + ;; + ollama-remote) + local model_name + if [[ "$SELECTED_MODEL" == "v4" ]]; then + model_name="sellerscrisp/jina-embeddings-v4-text-code-q4" + else + model_name="unclemusclez/jina-embeddings-v2-base-code" + fi + cat >> "$config_file" << EOF + ollama: + url: "$OLLAMA_REMOTE_URL" + model: "$model_name" +EOF + ;; + # ... rest unchanged + esac +``` + +### Step 11.5: Update setup_embedding_model to use selected model + +```bash +setup_embedding_model() { + if [[ "$SELECTED_PROVIDER" != "ollama" ]] || [[ "$OLLAMA_INSTALLED" != "true" ]]; then + return + fi + + step "[5/5] Setting up embedding model..." + echo "" + + local MODEL + local SIZE + if [[ "$SELECTED_MODEL" == "v4" ]]; then + MODEL="sellerscrisp/jina-embeddings-v4-text-code-q4" + SIZE="~8GB" + else + MODEL="unclemusclez/jina-embeddings-v2-base-code" + SIZE="~300MB" + fi + + info "Pulling embedding model: $MODEL" + info "This may take a few minutes on first run ($SIZE)..." + + # Check if Ollama is running + if ! curl -s http://localhost:11434/ > /dev/null 2>&1; then + warn "Ollama is not running. Starting Ollama..." + if [[ "$OS" == "darwin" ]]; then + open -a Ollama 2>/dev/null || ollama serve & + else + ollama serve & + fi + sleep 3 + fi + + ollama pull "$MODEL" || warn "Failed to pull model. Run 'ollama pull $MODEL' manually." + success "Embedding model ready" +} +``` + +### Step 11.6: Commit + +```bash +git add scripts/install.sh +git commit -m "feat(install): add model selection for Ollama provider" +``` + +--- + +## Task 12: Update PowerShell Install Script + +**Files:** +- Modify: `scripts/install.ps1` + +### Step 12.1: Add model selection variable and function + +Add after `$script:OLLAMA_INSTALLED = $false`: + +```powershell +$script:SELECTED_MODEL = "v2" + +function Select-OllamaModel { + Write-Host "" + Write-Host " Which embedding model?" + Write-Host "" + Write-Host " 1) Standard - Jina v2 Code (~300MB, faster, good quality) (Recommended)" + Write-Host " 2) Maximum - Jina v4 Code (~8GB, slower, best quality)" + Write-Host "" + $choice = Read-Host " Choice [1]" + if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } + + switch ($choice) { + "1" { + $script:SELECTED_MODEL = "v2" + Write-Success "Selected: Jina v2 Code (Standard)" + } + "2" { + $script:SELECTED_MODEL = "v4" + Write-Success "Selected: Jina v4 Code (Maximum quality)" + } + default { + Write-Warning "Invalid choice. Using Standard (v2)." + $script:SELECTED_MODEL = "v2" + } + } +} +``` + +### Step 12.2: Call Select-OllamaModel in Setup-LocalOllama and Setup-RemoteOllama + +Update the functions similar to bash script. + +### Step 12.3: Update Write-GlobalConfig and Setup-EmbeddingModel + +Similar updates as bash script. + +### Step 12.4: Commit + +```bash +git add scripts/install.ps1 +git commit -m "feat(install): add model selection for PowerShell installer" +``` + +--- + +## Task 13: Update Version to 0.8.0 + +**Files:** +- Modify: `cmd/pm/main.go` +- Modify: `cmd/pommeld/main.go` +- Modify: `internal/cli/root.go` + +### Step 13.1: Update version strings + +In all three files, change version from "0.7.3" to "0.8.0". + +### Step 13.2: Commit + +```bash +git add cmd/pm/main.go cmd/pommeld/main.go internal/cli/root.go +git commit -m "chore: bump version to 0.8.0" +``` + +--- + +## Task 14: Run Full Test Suite + +### Step 14.1: Run all tests + +```bash +go test -tags fts5 ./... -v +``` + +Expected: All PASS + +### Step 14.2: Build binaries + +```bash +go build -tags fts5 -o pm ./cmd/pm +go build -tags fts5 -o pommeld ./cmd/pommeld +``` + +Expected: Success + +### Step 14.3: Manual smoke test + +```bash +./pm version +./pm config model +``` + +--- + +## Task 15: Final Commit and Push + +### Step 15.1: Ensure all changes committed + +```bash +git status +``` + +### Step 15.2: Push branch + +```bash +git push -u origin dev-jina-4 +``` + +### Step 15.3: Create PR + +```bash +gh pr create --base dev --title "feat: add Jina v4 embedding model support" --body "..." +``` + +--- + +## Summary + +| Task | Description | Tests | +|------|-------------|-------| +| 1 | Model registry basics | 5 | +| 2 | GetModelByFullName | 3 | +| 3 | GetDimensionsForModel | 4 | +| 4 | OllamaClient dimensions | 3 | +| 5 | OllamaClient context size | 2 | +| 6 | GetShortNameForModel | 3 | +| 7 | pm config model (show) | 1 | +| 8 | pm config model (switch) | 1 | +| 9 | Invalid model error | 1 | +| 10 | Same model no-op | 1 | +| 11-12 | Install scripts | Manual | +| 13-15 | Version, tests, PR | - | + +**Total unit tests: 24** diff --git a/internal/cli/config_model.go b/internal/cli/config_model.go new file mode 100644 index 0000000..103ac98 --- /dev/null +++ b/internal/cli/config_model.go @@ -0,0 +1,103 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pommel-dev/pommel/internal/config" + "github.com/pommel-dev/pommel/internal/db" + "github.com/pommel-dev/pommel/internal/embedder" + "github.com/spf13/cobra" +) + +func init() { + configCmd.AddCommand(newConfigModelCmd()) +} + +func newConfigModelCmd() *cobra.Command { + return &cobra.Command{ + Use: "model [v2|v4]", + Short: "View or change the embedding model", + Long: `View or change the embedding model used for code search. + +Without arguments, shows the current model. +With an argument, switches to the specified model. + +Warning: Switching models deletes the existing index and requires reindexing. + +Available models: + v2 - Jina v2 Code (~300MB, 768 dims) - lightweight, fast + v4 - Jina v4 Code (~8GB, 1024 dims) - best quality, larger`, + RunE: runConfigModel, + } +} + +func runConfigModel(cmd *cobra.Command, args []string) error { + loader := config.NewLoader(projectRoot) + cfg, err := loader.Load() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + // No args - show current model + if len(args) == 0 { + return showCurrentModel(cmd, cfg) + } + + // Switch to requested model + return switchModel(cmd, loader, cfg, args[0]) +} + +func showCurrentModel(cmd *cobra.Command, cfg *config.Config) error { + modelName := cfg.Embedding.Ollama.Model + if modelName == "" { + modelName = embedder.EmbeddingModels[embedder.DefaultModel].Name + } + + shortName := embedder.GetShortNameForModel(modelName) + if shortName == "" { + shortName = "custom" + } + + fmt.Fprintf(cmd.OutOrStdout(), "Current model: %s (%s)\n", shortName, modelName) + return nil +} + +func switchModel(cmd *cobra.Command, loader *config.Loader, cfg *config.Config, target string) error { + // Validate target model + targetInfo, err := embedder.GetModelInfo(target) + if err != nil { + return err + } + + // Check if already using this model + currentModel := cfg.Embedding.Ollama.Model + if currentModel == targetInfo.Name { + shortName := embedder.GetShortNameForModel(currentModel) + fmt.Fprintf(cmd.OutOrStdout(), "Already using model %s\n", shortName) + return nil + } + + // Check for existing database and delete if present + dbPath := filepath.Join(projectRoot, ".pommel", db.DatabaseFile) + if _, err := os.Stat(dbPath); err == nil { + if err := os.Remove(dbPath); err != nil { + return fmt.Errorf("failed to delete existing database: %w", err) + } + // Clean up WAL/SHM files (ignore errors - they may not exist) + os.Remove(dbPath + "-wal") + os.Remove(dbPath + "-shm") + } + + // Update config + cfg.Embedding.Ollama.Model = targetInfo.Name + if err := loader.Save(cfg); err != nil { + return fmt.Errorf("failed to save configuration: %w", err) + } + + shortName := embedder.GetShortNameForModel(targetInfo.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Switched to model %s (%s)\n", shortName, targetInfo.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Run 'pm start' to reindex with the new model.\n") + return nil +} diff --git a/internal/cli/config_model_test.go b/internal/cli/config_model_test.go new file mode 100644 index 0000000..9e549b3 --- /dev/null +++ b/internal/cli/config_model_test.go @@ -0,0 +1,287 @@ +package cli + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/pommel-dev/pommel/internal/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigModel_ShowCurrent_Default(t *testing.T) { + // Setup: create temp project with default config + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Save and restore original projectRoot + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "v2") + assert.Contains(t, stdout.String(), "unclemusclez/jina-embeddings-v2-base-code") +} + +func TestConfigModel_ShowCurrent_V4(t *testing.T) { + // Setup: create temp project with v4 config + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "sellerscrisp/jina-embeddings-v4-text-code-q4" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Save and restore original projectRoot + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "v4") + assert.Contains(t, stdout.String(), "sellerscrisp/jina-embeddings-v4-text-code-q4") +} + +func TestConfigModel_ShowCurrent_CustomModel(t *testing.T) { + // Setup: create temp project with custom model + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "my-custom-embedding-model" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Save and restore original projectRoot + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "custom") + assert.Contains(t, stdout.String(), "my-custom-embedding-model") +} + +func TestConfigModel_ShowCurrent_EmptyModel(t *testing.T) { + // Setup: create temp project with no model configured (should use default) + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Save and restore original projectRoot + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + // Should show default v2 model + assert.Contains(t, stdout.String(), "v2") + assert.Contains(t, stdout.String(), "unclemusclez/jina-embeddings-v2-base-code") +} + +func TestConfigModel_NoConfigFile(t *testing.T) { + // Setup: temp directory without config + projectDir := t.TempDir() + + // Save and restore original projectRoot + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + + // Assert - should fail because no config exists + require.Error(t, err) + assert.Contains(t, err.Error(), "config") +} + +func TestConfigModel_SetV4_NoExistingDB(t *testing.T) { + // Setup: create temp project with v2 config, no database + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + configPath := filepath.Join(pommelDir, "config.yaml") + require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644)) + + // Execute + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"v4"}) + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + err := cmd.Execute() + + // Assert + require.NoError(t, err) + assert.Contains(t, stdout.String(), "v4") + assert.Contains(t, stdout.String(), "pm start") + + // Verify config was updated + content, err := os.ReadFile(configPath) + require.NoError(t, err) + assert.Contains(t, string(content), "sellerscrisp/jina-embeddings-v4-text-code-q4") +} + +func TestConfigModel_SameModel(t *testing.T) { + // Setup: create temp project with v2 config + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"v2"}) + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + err := cmd.Execute() + + require.NoError(t, err) + assert.Contains(t, stdout.String(), "Already using") +} + +func TestConfigModel_InvalidModel(t *testing.T) { + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + cmd := newConfigModelCmd() + cmd.SetArgs([]string{"v5"}) + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + err := cmd.Execute() + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown model 'v5'") +} + +func TestConfigModel_DeletesExistingDB(t *testing.T) { + // Setup: create temp project with v2 config AND existing database + projectDir := t.TempDir() + pommelDir := filepath.Join(projectDir, ".pommel") + require.NoError(t, os.MkdirAll(pommelDir, 0755)) + + configContent := `version: "1" +embedding: + provider: ollama + ollama: + model: "unclemusclez/jina-embeddings-v2-base-code" +` + require.NoError(t, os.WriteFile(filepath.Join(pommelDir, "config.yaml"), []byte(configContent), 0644)) + + // Create a dummy database file + dbPath := filepath.Join(pommelDir, db.DatabaseFile) + require.NoError(t, os.WriteFile(dbPath, []byte("dummy db content"), 0644)) + + var stdout bytes.Buffer + cmd := newConfigModelCmd() + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"v4"}) + origProjectRoot := projectRoot + defer func() { projectRoot = origProjectRoot }() + projectRoot = projectDir + + err := cmd.Execute() + + require.NoError(t, err) + // Verify database was deleted + _, err = os.Stat(dbPath) + assert.True(t, os.IsNotExist(err), "database should be deleted") +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 5bdda17..415ac43 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -8,7 +8,7 @@ import ( ) var ( - Version = "0.7.3" + Version = "0.8.0" BuildCommit = "unknown" BuildDate = "unknown" diff --git a/internal/embedder/models.go b/internal/embedder/models.go new file mode 100644 index 0000000..a5fa3be --- /dev/null +++ b/internal/embedder/models.go @@ -0,0 +1,96 @@ +package embedder + +import ( + "fmt" + "strings" +) + +// ModelInfo contains metadata about an embedding model. +type ModelInfo struct { + Name string // Full Ollama model name + Dimensions int // Embedding vector dimensions + ContextSize int // Maximum context window in tokens + Size string // Human-readable size (e.g., "~300MB") +} + +// EmbeddingModels maps short names to model info. +var EmbeddingModels = map[string]ModelInfo{ + "v2": { + Name: "unclemusclez/jina-embeddings-v2-base-code", + Dimensions: 768, + ContextSize: 8192, + Size: "~300MB", + }, + "v4": { + Name: "sellerscrisp/jina-embeddings-v4-text-code-q4", + Dimensions: 1024, + ContextSize: 32768, + Size: "~8GB", + }, +} + +// DefaultModel is the short name of the default embedding model. +const DefaultModel = "v2" + +// DefaultDimensions is the fallback for unknown models. +const DefaultDimensions = 768 + +// DefaultContextSize is the fallback for unknown models. +const DefaultContextSize = 8192 + +// GetModelInfo returns model info by short name (v2, v4). +func GetModelInfo(shortName string) (*ModelInfo, error) { + shortName = strings.ToLower(strings.TrimSpace(shortName)) + if shortName == "" { + return nil, fmt.Errorf("model name cannot be empty") + } + info, ok := EmbeddingModels[shortName] + if !ok { + return nil, fmt.Errorf("unknown model '%s', use v2 or v4", shortName) + } + return &info, nil +} + +// GetModelByFullName returns model info by full Ollama model name. +// Returns nil if the model is not in our registry (unknown model). +func GetModelByFullName(fullName string) *ModelInfo { + fullName = strings.TrimSpace(fullName) + for _, info := range EmbeddingModels { + if info.Name == fullName { + return &info + } + } + return nil +} + +// GetDimensionsForModel returns dimensions for a model by full name. +// Returns DefaultDimensions (768) for unknown models. +func GetDimensionsForModel(fullName string) int { + info := GetModelByFullName(fullName) + if info == nil { + return DefaultDimensions + } + return info.Dimensions +} + +// GetContextSizeForModel returns context size for a model by full name. +// Returns DefaultContextSize (8192) for unknown models. +func GetContextSizeForModel(fullName string) int { + info := GetModelByFullName(fullName) + if info == nil { + return DefaultContextSize + } + return info.ContextSize +} + +// GetShortNameForModel returns the short name (v2, v4) for a full Ollama model name. +// Returns empty string if the model is not in our registry. +func GetShortNameForModel(fullName string) string { + fullName = strings.TrimSpace(fullName) + for shortName, info := range EmbeddingModels { + if info.Name == fullName { + return shortName + } + } + return "" +} diff --git a/internal/embedder/models_test.go b/internal/embedder/models_test.go new file mode 100644 index 0000000..8e252e9 --- /dev/null +++ b/internal/embedder/models_test.go @@ -0,0 +1,114 @@ +package embedder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetModelInfo_V2(t *testing.T) { + info, err := GetModelInfo("v2") + require.NoError(t, err) + assert.Equal(t, "unclemusclez/jina-embeddings-v2-base-code", info.Name) + assert.Equal(t, 768, info.Dimensions) + assert.Equal(t, 8192, info.ContextSize) +} + +func TestGetModelInfo_V4(t *testing.T) { + info, err := GetModelInfo("v4") + require.NoError(t, err) + assert.Equal(t, "sellerscrisp/jina-embeddings-v4-text-code-q4", info.Name) + assert.Equal(t, 1024, info.Dimensions) + assert.Equal(t, 32768, info.ContextSize) +} + +func TestGetModelInfo_UnknownShortName(t *testing.T) { + info, err := GetModelInfo("v5") + assert.Nil(t, info) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown model 'v5'") +} + +func TestGetModelInfo_EmptyName(t *testing.T) { + info, err := GetModelInfo("") + assert.Nil(t, info) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be empty") +} + +func TestGetModelInfo_CaseInsensitive(t *testing.T) { + // Test uppercase "V4" works the same as lowercase "v4" + info, err := GetModelInfo("V4") + require.NoError(t, err) + assert.Equal(t, "sellerscrisp/jina-embeddings-v4-text-code-q4", info.Name) + assert.Equal(t, 1024, info.Dimensions) + assert.Equal(t, 32768, info.ContextSize) +} + +func TestGetModelByFullName_V2(t *testing.T) { + info := GetModelByFullName("unclemusclez/jina-embeddings-v2-base-code") + require.NotNil(t, info) + assert.Equal(t, 768, info.Dimensions) +} + +func TestGetModelByFullName_V4(t *testing.T) { + info := GetModelByFullName("sellerscrisp/jina-embeddings-v4-text-code-q4") + require.NotNil(t, info) + assert.Equal(t, 1024, info.Dimensions) +} + +func TestGetModelByFullName_Unknown(t *testing.T) { + info := GetModelByFullName("some-random-model") + assert.Nil(t, info) +} + +func TestGetDimensionsForModel_V2(t *testing.T) { + dims := GetDimensionsForModel("unclemusclez/jina-embeddings-v2-base-code") + assert.Equal(t, 768, dims) +} + +func TestGetDimensionsForModel_V4(t *testing.T) { + dims := GetDimensionsForModel("sellerscrisp/jina-embeddings-v4-text-code-q4") + assert.Equal(t, 1024, dims) +} + +func TestGetDimensionsForModel_Unknown_DefaultsTo768(t *testing.T) { + dims := GetDimensionsForModel("some-unknown-model") + assert.Equal(t, 768, dims, "unknown models should default to 768") +} + +func TestGetDimensionsForModel_Empty_DefaultsTo768(t *testing.T) { + dims := GetDimensionsForModel("") + assert.Equal(t, 768, dims, "empty model should default to 768") +} + +func TestGetShortNameForModel_V2(t *testing.T) { + shortName := GetShortNameForModel("unclemusclez/jina-embeddings-v2-base-code") + assert.Equal(t, "v2", shortName) +} + +func TestGetShortNameForModel_V4(t *testing.T) { + shortName := GetShortNameForModel("sellerscrisp/jina-embeddings-v4-text-code-q4") + assert.Equal(t, "v4", shortName) +} + +func TestGetShortNameForModel_Unknown(t *testing.T) { + shortName := GetShortNameForModel("some-random-model") + assert.Equal(t, "", shortName, "unknown models should return empty string") +} + +func TestGetContextSizeForModel_V2(t *testing.T) { + size := GetContextSizeForModel("unclemusclez/jina-embeddings-v2-base-code") + assert.Equal(t, 8192, size) +} + +func TestGetContextSizeForModel_V4(t *testing.T) { + size := GetContextSizeForModel("sellerscrisp/jina-embeddings-v4-text-code-q4") + assert.Equal(t, 32768, size) +} + +func TestGetContextSizeForModel_Unknown(t *testing.T) { + size := GetContextSizeForModel("unknown-model") + assert.Equal(t, 8192, size, "unknown models should default to 8192") +} diff --git a/internal/embedder/ollama.go b/internal/embedder/ollama.go index a220794..3524e84 100644 --- a/internal/embedder/ollama.go +++ b/internal/embedder/ollama.go @@ -57,11 +57,6 @@ type ollamaEmbedRequest struct { Options map[string]interface{} `json:"options,omitempty"` } -// JinaContextSize is the full context window for Jina embeddings v2. -// By default, Ollama may use a smaller context (4096), so we explicitly -// request the full 8192 to avoid "input length exceeds context length" errors. -const JinaContextSize = 8192 - // ollamaEmbedResponse represents the response from Ollama's /api/embed endpoint. type ollamaEmbedResponse struct { Embeddings [][]float32 `json:"embeddings"` @@ -157,7 +152,7 @@ func (c *OllamaClient) embed(ctx context.Context, input any) ([][]float32, error Model: c.model, Input: input, Options: map[string]interface{}{ - "num_ctx": JinaContextSize, + "num_ctx": c.ContextSize(), }, } @@ -304,7 +299,12 @@ func (c *OllamaClient) ModelName() string { return c.model } -// Dimensions returns the embedding dimension size (768 for Jina Code). +// Dimensions returns the embedding dimension size based on the configured model. func (c *OllamaClient) Dimensions() int { - return 768 + return GetDimensionsForModel(c.model) +} + +// ContextSize returns the context window size based on the configured model. +func (c *OllamaClient) ContextSize() int { + return GetContextSizeForModel(c.model) } diff --git a/internal/embedder/ollama_test.go b/internal/embedder/ollama_test.go index 17ccdac..b586efc 100644 --- a/internal/embedder/ollama_test.go +++ b/internal/embedder/ollama_test.go @@ -242,6 +242,54 @@ func TestOllamaClient_Dimensions(t *testing.T) { assert.Equal(t, 768, dims, "Dimensions should return 768 for Jina Code embeddings") } +func TestOllamaClient_Dimensions_V2Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "unclemusclez/jina-embeddings-v2-base-code", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 768, client.Dimensions()) +} + +func TestOllamaClient_Dimensions_V4Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "sellerscrisp/jina-embeddings-v4-text-code-q4", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 1024, client.Dimensions()) +} + +func TestOllamaClient_Dimensions_UnknownModel(t *testing.T) { + cfg := OllamaConfig{ + Model: "some-random-model", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 768, client.Dimensions(), "unknown models default to 768") +} + +func TestOllamaClient_ContextSize_V2Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "unclemusclez/jina-embeddings-v2-base-code", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 8192, client.ContextSize()) +} + +func TestOllamaClient_ContextSize_V4Model(t *testing.T) { + cfg := OllamaConfig{ + Model: "sellerscrisp/jina-embeddings-v4-text-code-q4", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 32768, client.ContextSize()) +} + +func TestOllamaClient_ContextSize_UnknownModel(t *testing.T) { + cfg := OllamaConfig{ + Model: "some-random-model", + } + client := NewOllamaClient(cfg) + assert.Equal(t, 8192, client.ContextSize(), "unknown models default to 8192") +} + func TestOllamaClient_ModelName(t *testing.T) { modelName := "unclemusclez/jina-embeddings-v2-base-code" @@ -616,5 +664,49 @@ func TestOllamaClient_SetsNumCtxOption(t *testing.T) { // JSON numbers decode as float64 numCtxFloat, ok := numCtx.(float64) require.True(t, ok, "num_ctx should be a number") - assert.Equal(t, float64(8192), numCtxFloat, "num_ctx should be 8192 for full Jina context") + // Unknown model defaults to 8192 + assert.Equal(t, float64(8192), numCtxFloat, "num_ctx should use model's context size (8192 default)") +} + +func TestOllamaClient_SetsNumCtxOption_V4Model(t *testing.T) { + var receivedOptions map[string]interface{} + + server := createMockOllamaServer(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/embed" { + w.WriteHeader(http.StatusNotFound) + return + } + + var req mockOllamaRequest + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + + // Capture the options for verification + receivedOptions = req.Options + + resp := mockOllamaResponse{ + Embeddings: [][]float32{generate768DimEmbedding()}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + }) + defer server.Close() + + client := NewOllamaClient(OllamaConfig{ + BaseURL: server.URL, + Model: "sellerscrisp/jina-embeddings-v4-text-code-q4", + Timeout: 5 * time.Second, + }) + + _, err := client.EmbedSingle(context.Background(), "test code") + require.NoError(t, err) + + // Verify num_ctx was sent with v4 context size + require.NotNil(t, receivedOptions, "Options should be sent in request") + numCtx, ok := receivedOptions["num_ctx"] + require.True(t, ok, "num_ctx should be present in options") + + numCtxFloat, ok := numCtx.(float64) + require.True(t, ok, "num_ctx should be a number") + assert.Equal(t, float64(32768), numCtxFloat, "num_ctx should be 32768 for v4 model") } diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 5d0a50c..63916c2 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -56,6 +56,7 @@ $script:OpenAIApiKey = "" $script:VoyageApiKey = "" $script:IsUpgrade = $false $script:CurrentVersion = "" +$script:SelectedModel = "v2" # Default to v2 #region Output Functions function Write-Step { @@ -180,6 +181,9 @@ function Select-Provider { function Setup-LocalOllama { $script:SelectedProvider = "ollama" Write-Success "Selected: Local Ollama" + + # Select model + Select-OllamaModel } function Setup-RemoteOllama { @@ -195,6 +199,10 @@ function Setup-RemoteOllama { $script:OllamaRemoteUrl = $url Write-Success "Selected: Remote Ollama at $url" + + # Select model + Select-OllamaModel + Write-Warn "Make sure the selected model is available on the remote server" } function Setup-OpenAI { @@ -241,6 +249,33 @@ function Setup-Voyage { } } +function Select-OllamaModel { + Write-Host "" + Write-Host " Which embedding model?" + Write-Host "" + Write-Host " 1) Standard - Jina v2 Code (~300MB, faster, good quality) (Recommended)" + Write-Host " 2) Maximum - Jina v4 Code (~8GB, slower, best quality)" + Write-Host "" + + $choice = Read-Host " Choice [1]" + if ([string]::IsNullOrEmpty($choice)) { $choice = "1" } + + switch ($choice) { + "1" { + $script:SelectedModel = "v2" + Write-Success "Selected: Jina v2 Code (Standard)" + } + "2" { + $script:SelectedModel = "v4" + Write-Success "Selected: Jina v4 Code (Maximum quality)" + } + default { + Write-Warn "Invalid choice. Using Standard (v2)." + $script:SelectedModel = "v2" + } + } +} + function Test-OpenAIKey { param([string]$Key) @@ -303,19 +338,29 @@ embedding: switch ($script:SelectedProvider) { "ollama" { + $modelName = if ($script:SelectedModel -eq "v4") { + "sellerscrisp/jina-embeddings-v4-text-code-q4" + } else { + "unclemusclez/jina-embeddings-v2-base-code" + } $yaml += @" ollama: url: "http://localhost:11434" - model: "unclemusclez/jina-embeddings-v2-base-code" + model: "$modelName" "@ } "ollama-remote" { + $modelName = if ($script:SelectedModel -eq "v4") { + "sellerscrisp/jina-embeddings-v4-text-code-q4" + } else { + "unclemusclez/jina-embeddings-v2-base-code" + } $yaml += @" ollama: url: "$($script:OllamaRemoteUrl)" - model: "unclemusclez/jina-embeddings-v2-base-code" + model: "$modelName" "@ } "openai" { @@ -717,7 +762,16 @@ function Install-Ollama { #region Model Installation function Install-EmbeddingModel { - Write-Step "Pulling embedding model (this may take a few minutes)..." + # Determine model based on selection + $modelName = if ($script:SelectedModel -eq "v4") { + "sellerscrisp/jina-embeddings-v4-text-code-q4" + } else { + "unclemusclez/jina-embeddings-v2-base-code" + } + $modelSize = if ($script:SelectedModel -eq "v4") { "~8GB" } else { "~300MB" } + + Write-Step "Pulling embedding model: $modelName ($modelSize)..." + Write-Step "This may take a few minutes on first run..." try { # Find ollama executable @@ -729,7 +783,7 @@ function Install-EmbeddingModel { } else { Write-Warn "Cannot find ollama executable" - Write-Host " Run manually: ollama pull $script:OllamaModel" + Write-Host " Run manually: ollama pull $modelName" return $false } } @@ -777,10 +831,10 @@ function Install-EmbeddingModel { # Pull the model Write-Host "" if ($ollamaCmd -is [System.Management.Automation.CommandInfo]) { - & $ollamaCmd.Source pull $script:OllamaModel + & $ollamaCmd.Source pull $modelName } else { - & $ollamaCmd pull $script:OllamaModel + & $ollamaCmd pull $modelName } if ($LASTEXITCODE -eq 0) { @@ -793,7 +847,7 @@ function Install-EmbeddingModel { } catch { Write-Warn "Failed to pull model: $_" - Write-Host " Run manually: ollama pull $script:OllamaModel" + Write-Host " Run manually: ollama pull $modelName" return $false } } diff --git a/scripts/install.sh b/scripts/install.sh index 80bc196..68785d0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -41,6 +41,7 @@ OLLAMA_REMOTE_URL="" OPENAI_API_KEY="" VOYAGE_API_KEY="" OLLAMA_INSTALLED=false +SELECTED_MODEL="v2" # Default to v2 INSTALL_DIR="" OS="" ARCH="" @@ -170,6 +171,9 @@ setup_local_ollama() { success "Ollama found" OLLAMA_INSTALLED=true fi + + # Select model + select_ollama_model } setup_remote_ollama() { @@ -185,6 +189,10 @@ setup_remote_ollama() { OLLAMA_REMOTE_URL="$url" success "Selected: Remote Ollama at $url" + + # Select model + select_ollama_model + warn "Make sure the selected model is available on the remote server" } setup_openai() { @@ -227,6 +235,32 @@ setup_voyage() { fi } +select_ollama_model() { + echo "" + echo " Which embedding model?" + echo "" + echo " 1) Standard - Jina v2 Code (~300MB, faster, good quality) (Recommended)" + echo " 2) Maximum - Jina v4 Code (~8GB, slower, best quality)" + echo "" + read -p " Choice [1]: " model_choice < /dev/tty + model_choice=${model_choice:-1} + + case $model_choice in + 1) + SELECTED_MODEL="v2" + success "Selected: Jina v2 Code (Standard)" + ;; + 2) + SELECTED_MODEL="v4" + success "Selected: Jina v4 Code (Maximum quality)" + ;; + *) + warn "Invalid choice. Using Standard (v2)." + SELECTED_MODEL="v2" + ;; + esac +} + validate_openai_key() { local key="$1" local response @@ -290,17 +324,29 @@ EOF case $SELECTED_PROVIDER in ollama) + local model_name + if [[ "$SELECTED_MODEL" == "v4" ]]; then + model_name="sellerscrisp/jina-embeddings-v4-text-code-q4" + else + model_name="unclemusclez/jina-embeddings-v2-base-code" + fi cat >> "$config_file" << EOF ollama: url: "http://localhost:11434" - model: "unclemusclez/jina-embeddings-v2-base-code" + model: "$model_name" EOF ;; ollama-remote) + local model_name + if [[ "$SELECTED_MODEL" == "v4" ]]; then + model_name="sellerscrisp/jina-embeddings-v4-text-code-q4" + else + model_name="unclemusclez/jina-embeddings-v2-base-code" + fi cat >> "$config_file" << EOF ollama: url: "$OLLAMA_REMOTE_URL" - model: "unclemusclez/jina-embeddings-v2-base-code" + model: "$model_name" EOF ;; openai) @@ -461,10 +507,18 @@ setup_embedding_model() { step "[5/5] Setting up embedding model..." echo "" - MODEL="unclemusclez/jina-embeddings-v2-base-code" + local MODEL + local SIZE + if [[ "$SELECTED_MODEL" == "v4" ]]; then + MODEL="sellerscrisp/jina-embeddings-v4-text-code-q4" + SIZE="~8GB" + else + MODEL="unclemusclez/jina-embeddings-v2-base-code" + SIZE="~300MB" + fi info "Pulling embedding model: $MODEL" - info "This may take a few minutes on first run (~300MB)..." + info "This may take a few minutes on first run ($SIZE)..." # Check if Ollama is running if ! curl -s http://localhost:11434/ > /dev/null 2>&1; then