feat(EMBEDDINGS-0001): enable local embedder by default and overhaul settings page

- Wire local embedding provider as the default on startup when no profile is configured
- Refactor embedding settings into dedicated service, DTOs, mappers and models
- Rebuild settings page with profile management UI and live test feedback
- Expose index summary (indexed versions + embedding count) on repo endpoints
- Harden indexing pipeline and context search with additional test coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-28 09:28:01 +01:00
parent d1381f7fc0
commit 781d224adc
30 changed files with 1419 additions and 313 deletions

View File

@@ -27,16 +27,20 @@ function createTestDb(): Database.Database {
client.pragma('foreign_keys = ON');
const migrationsFolder = join(import.meta.dirname, '../db/migrations');
const migrationSql = readFileSync(join(migrationsFolder, '0000_large_master_chief.sql'), 'utf-8');
// Drizzle migration files use `--> statement-breakpoint` as separator.
const statements = migrationSql
.split('--> statement-breakpoint')
.map((s) => s.trim())
.filter(Boolean);
for (const migration of [
'0000_large_master_chief.sql',
'0001_quick_nighthawk.sql',
'0002_silky_stellaris.sql'
]) {
const statements = readFileSync(join(migrationsFolder, migration), 'utf-8')
.split('--> statement-breakpoint')
.map((statement) => statement.trim())
.filter(Boolean);
for (const stmt of statements) {
client.exec(stmt);
for (const statement of statements) {
client.exec(statement);
}
}
return client;
@@ -408,6 +412,83 @@ describe('RepositoryService.getVersions()', () => {
});
});
// ---------------------------------------------------------------------------
// getIndexSummary()
// ---------------------------------------------------------------------------
describe('RepositoryService.getIndexSummary()', () => {
let client: Database.Database;
let service: RepositoryService;
beforeEach(() => {
client = createTestDb();
service = makeService(client);
service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react', branch: 'main' });
});
it('returns embedding counts and indexed version labels', () => {
const now = Math.floor(Date.now() / 1000);
const docId = crypto.randomUUID();
const versionDocId = crypto.randomUUID();
const snippetId = crypto.randomUUID();
const versionSnippetId = crypto.randomUUID();
client
.prepare(
`INSERT INTO repository_versions (id, repository_id, tag, state, created_at)
VALUES (?, '/facebook/react', ?, 'indexed', ?)`
)
.run('/facebook/react/v18.3.0', 'v18.3.0', now);
client
.prepare(
`INSERT INTO documents (id, repository_id, version_id, file_path, checksum, indexed_at)
VALUES (?, '/facebook/react', NULL, 'README.md', 'base', ?)`
)
.run(docId, now);
client
.prepare(
`INSERT INTO documents (id, repository_id, version_id, file_path, checksum, indexed_at)
VALUES (?, '/facebook/react', ?, 'README.md', 'version', ?)`
)
.run(versionDocId, '/facebook/react/v18.3.0', now);
client
.prepare(
`INSERT INTO snippets (id, document_id, repository_id, version_id, type, content, created_at)
VALUES (?, ?, '/facebook/react', NULL, 'info', 'base snippet', ?)`
)
.run(snippetId, docId, now);
client
.prepare(
`INSERT INTO snippets (id, document_id, repository_id, version_id, type, content, created_at)
VALUES (?, ?, '/facebook/react', ?, 'info', 'version snippet', ?)`
)
.run(versionSnippetId, versionDocId, '/facebook/react/v18.3.0', now);
client
.prepare(
`INSERT INTO snippet_embeddings (snippet_id, profile_id, model, dimensions, embedding, created_at)
VALUES (?, 'local-default', 'Xenova/all-MiniLM-L6-v2', 2, ?, ?)`
)
.run(snippetId, Buffer.from(Float32Array.from([1, 0]).buffer), now);
client
.prepare(
`INSERT INTO snippet_embeddings (snippet_id, profile_id, model, dimensions, embedding, created_at)
VALUES (?, 'local-default', 'Xenova/all-MiniLM-L6-v2', 2, ?, ?)`
)
.run(versionSnippetId, Buffer.from(Float32Array.from([0, 1]).buffer), now);
expect(service.getIndexSummary('/facebook/react')).toEqual({
embeddingCount: 2,
indexedVersions: ['main', 'v18.3.0']
});
});
});
// ---------------------------------------------------------------------------
// createIndexingJob()
// ---------------------------------------------------------------------------