- Add migration 0003: recreate repository_configs with nullable version_id column and two partial unique indexes (repo-wide: version_id IS NULL, per-version: (repository_id, version_id) WHERE version_id IS NOT NULL) - Update schema.ts to reflect the new composite structure with uniqueIndex partial constraints via drizzle-orm sql helper - IndexingPipeline: parse trueref.json / context7.json after crawl, apply excludeFiles filter before diff computation, update totalFiles accordingly - IndexingPipeline: persist repo-wide rules (version_id=null) and version-specific rules (when versionId set) via upsertRepoConfig helper - Add matchesExcludePattern static helper supporting plain filename, glob prefix (docs/legacy*), and exact path patterns - context endpoint: split getRules into repo-wide + version-specific lookup with dedup merge; pass versionId at call site - Update test DB loaders to include migration 0003 - Add pipeline tests for excludeFiles, repo-wide rules persistence, and per-version rules persistence - Add integration tests for merged rules, repo-only rules, and dedup logic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
9.3 KiB
TypeScript
226 lines
9.3 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
||
import {
|
||
blob,
|
||
integer,
|
||
primaryKey,
|
||
real,
|
||
sqliteTable,
|
||
text,
|
||
uniqueIndex
|
||
} from 'drizzle-orm/sqlite-core';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// repositories
|
||
// ---------------------------------------------------------------------------
|
||
export const repositories = sqliteTable('repositories', {
|
||
id: text('id').primaryKey(), // e.g. "/facebook/react" or "/local/my-sdk"
|
||
title: text('title').notNull(),
|
||
description: text('description'),
|
||
source: text('source', { enum: ['github', 'local'] }).notNull(),
|
||
sourceUrl: text('source_url').notNull(), // GitHub URL or absolute local path
|
||
branch: text('branch').default('main'),
|
||
state: text('state', {
|
||
enum: ['pending', 'indexing', 'indexed', 'error']
|
||
})
|
||
.notNull()
|
||
.default('pending'),
|
||
totalSnippets: integer('total_snippets').default(0),
|
||
totalTokens: integer('total_tokens').default(0),
|
||
trustScore: real('trust_score').default(0), // 0.0–10.0
|
||
benchmarkScore: real('benchmark_score').default(0), // 0.0–100.0; reserved for future quality metrics
|
||
stars: integer('stars'),
|
||
// TODO: encrypt at rest in production; stored as plaintext for v1
|
||
githubToken: text('github_token'),
|
||
lastIndexedAt: integer('last_indexed_at', { mode: 'timestamp' }),
|
||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// repository_versions
|
||
// ---------------------------------------------------------------------------
|
||
export const repositoryVersions = sqliteTable('repository_versions', {
|
||
id: text('id').primaryKey(), // e.g. "/facebook/react/v18.3.0"
|
||
repositoryId: text('repository_id')
|
||
.notNull()
|
||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||
tag: text('tag').notNull(), // git tag or branch name
|
||
title: text('title'),
|
||
commitHash: text('commit_hash'), // immutable commit SHA-1 resolved from tag
|
||
state: text('state', {
|
||
enum: ['pending', 'indexing', 'indexed', 'error']
|
||
})
|
||
.notNull()
|
||
.default('pending'),
|
||
totalSnippets: integer('total_snippets').default(0),
|
||
indexedAt: integer('indexed_at', { mode: 'timestamp' }),
|
||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// documents
|
||
// ---------------------------------------------------------------------------
|
||
export const documents = sqliteTable('documents', {
|
||
id: text('id').primaryKey(), // UUID
|
||
repositoryId: text('repository_id')
|
||
.notNull()
|
||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||
versionId: text('version_id').references(() => repositoryVersions.id, { onDelete: 'cascade' }),
|
||
filePath: text('file_path').notNull(), // relative path within repo
|
||
title: text('title'),
|
||
language: text('language'), // e.g. "typescript", "markdown"
|
||
tokenCount: integer('token_count').default(0),
|
||
checksum: text('checksum').notNull(), // SHA-256 of file content
|
||
indexedAt: integer('indexed_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// snippets
|
||
// ---------------------------------------------------------------------------
|
||
export const snippets = sqliteTable('snippets', {
|
||
id: text('id').primaryKey(), // UUID
|
||
documentId: text('document_id')
|
||
.notNull()
|
||
.references(() => documents.id, { onDelete: 'cascade' }),
|
||
repositoryId: text('repository_id')
|
||
.notNull()
|
||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||
versionId: text('version_id').references(() => repositoryVersions.id, { onDelete: 'cascade' }),
|
||
type: text('type', { enum: ['code', 'info'] }).notNull(),
|
||
title: text('title'),
|
||
content: text('content').notNull(), // searchable text / code
|
||
language: text('language'),
|
||
breadcrumb: text('breadcrumb'), // e.g. "Installation > Getting Started"
|
||
tokenCount: integer('token_count').default(0),
|
||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// embedding_profiles
|
||
// ---------------------------------------------------------------------------
|
||
export const embeddingProfiles = sqliteTable('embedding_profiles', {
|
||
id: text('id').primaryKey(),
|
||
providerKind: text('provider_kind').notNull(),
|
||
title: text('title').notNull(),
|
||
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
||
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
|
||
model: text('model').notNull(),
|
||
dimensions: integer('dimensions').notNull(),
|
||
config: text('config', { mode: 'json' }).notNull().$type<Record<string, unknown>>(),
|
||
createdAt: integer('created_at').notNull(),
|
||
updatedAt: integer('updated_at').notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// snippet_embeddings
|
||
// ---------------------------------------------------------------------------
|
||
export const snippetEmbeddings = sqliteTable(
|
||
'snippet_embeddings',
|
||
{
|
||
snippetId: text('snippet_id')
|
||
.notNull()
|
||
.references(() => snippets.id, { onDelete: 'cascade' }),
|
||
profileId: text('profile_id')
|
||
.notNull()
|
||
.references(() => embeddingProfiles.id, { onDelete: 'cascade' }),
|
||
model: text('model').notNull(), // embedding model identifier
|
||
dimensions: integer('dimensions').notNull(),
|
||
embedding: blob('embedding').notNull(), // Float32Array as binary blob
|
||
createdAt: integer('created_at').notNull()
|
||
},
|
||
(table) => [primaryKey({ columns: [table.snippetId, table.profileId] })]
|
||
);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// indexing_jobs
|
||
// ---------------------------------------------------------------------------
|
||
export const indexingJobs = sqliteTable('indexing_jobs', {
|
||
id: text('id').primaryKey(), // UUID
|
||
repositoryId: text('repository_id')
|
||
.notNull()
|
||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||
versionId: text('version_id'),
|
||
status: text('status', {
|
||
enum: ['queued', 'running', 'paused', 'cancelled', 'done', 'failed']
|
||
})
|
||
.notNull()
|
||
.default('queued'),
|
||
progress: integer('progress').default(0), // 0–100
|
||
totalFiles: integer('total_files').default(0),
|
||
processedFiles: integer('processed_files').default(0),
|
||
error: text('error'),
|
||
startedAt: integer('started_at', { mode: 'timestamp' }),
|
||
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// repository_configs
|
||
// ---------------------------------------------------------------------------
|
||
export const repositoryConfigs = sqliteTable(
|
||
'repository_configs',
|
||
{
|
||
repositoryId: text('repository_id')
|
||
.notNull()
|
||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||
versionId: text('version_id'),
|
||
projectTitle: text('project_title'),
|
||
description: text('description'),
|
||
folders: text('folders', { mode: 'json' }).$type<string[]>(),
|
||
excludeFolders: text('exclude_folders', { mode: 'json' }).$type<string[]>(),
|
||
excludeFiles: text('exclude_files', { mode: 'json' }).$type<string[]>(),
|
||
rules: text('rules', { mode: 'json' }).$type<string[]>(),
|
||
previousVersions: text('previous_versions', { mode: 'json' }).$type<
|
||
{ tag: string; title: string; commitHash?: string }[]
|
||
>(),
|
||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
|
||
},
|
||
(table) => [
|
||
uniqueIndex('uniq_repo_config_base')
|
||
.on(table.repositoryId)
|
||
.where(sql`${table.versionId} IS NULL`),
|
||
uniqueIndex('uniq_repo_config_version')
|
||
.on(table.repositoryId, table.versionId)
|
||
.where(sql`${table.versionId} IS NOT NULL`)
|
||
]
|
||
);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// settings
|
||
// ---------------------------------------------------------------------------
|
||
export const settings = sqliteTable('settings', {
|
||
key: text('key').primaryKey(),
|
||
value: text('value', { mode: 'json' }),
|
||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Inferred TypeScript types
|
||
// ---------------------------------------------------------------------------
|
||
export type Repository = typeof repositories.$inferSelect;
|
||
export type NewRepository = typeof repositories.$inferInsert;
|
||
|
||
export type RepositoryVersion = typeof repositoryVersions.$inferSelect;
|
||
export type NewRepositoryVersion = typeof repositoryVersions.$inferInsert;
|
||
|
||
export type Document = typeof documents.$inferSelect;
|
||
export type NewDocument = typeof documents.$inferInsert;
|
||
|
||
export type Snippet = typeof snippets.$inferSelect;
|
||
export type NewSnippet = typeof snippets.$inferInsert;
|
||
|
||
export type EmbeddingProfile = typeof embeddingProfiles.$inferSelect;
|
||
export type NewEmbeddingProfile = typeof embeddingProfiles.$inferInsert;
|
||
|
||
export type SnippetEmbedding = typeof snippetEmbeddings.$inferSelect;
|
||
export type NewSnippetEmbedding = typeof snippetEmbeddings.$inferInsert;
|
||
|
||
export type IndexingJob = typeof indexingJobs.$inferSelect;
|
||
export type NewIndexingJob = typeof indexingJobs.$inferInsert;
|
||
|
||
export type RepositoryConfig = typeof repositoryConfigs.$inferSelect;
|
||
export type NewRepositoryConfig = typeof repositoryConfigs.$inferInsert;
|
||
|
||
export type Settings = typeof settings.$inferSelect;
|
||
export type NewSettings = typeof settings.$inferInsert;
|