coderag-ai 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. coderag/__init__.py +12 -0
  2. coderag/analyzers/__init__.py +0 -0
  3. coderag/analyzers/base.py +36 -0
  4. coderag/analyzers/flake8_adapter.py +43 -0
  5. coderag/analyzers/pylint_adapter.py +50 -0
  6. coderag/analyzers/workflow.py +107 -0
  7. coderag/api/__init__.py +0 -0
  8. coderag/api/app.py +351 -0
  9. coderag/api/dashboard.py +237 -0
  10. coderag/api/schemas.py +181 -0
  11. coderag/cli/__init__.py +0 -0
  12. coderag/cli/main.py +359 -0
  13. coderag/context/__init__.py +0 -0
  14. coderag/context/builder.py +206 -0
  15. coderag/context/package.py +100 -0
  16. coderag/core/__init__.py +0 -0
  17. coderag/core/config.py +100 -0
  18. coderag/core/logging.py +58 -0
  19. coderag/core/text.py +80 -0
  20. coderag/core/tokens.py +66 -0
  21. coderag/db/__init__.py +0 -0
  22. coderag/db/base.py +58 -0
  23. coderag/db/migrations/env.py +77 -0
  24. coderag/db/migrations/script.py.mako +26 -0
  25. coderag/db/migrations/versions/.gitkeep +0 -0
  26. coderag/db/migrations/versions/9a973932b94d_measure_baseline_tokens.py +41 -0
  27. coderag/db/migrations/versions/c8a40feeda41_initial_schema.py +227 -0
  28. coderag/db/models.py +275 -0
  29. coderag/embeddings/__init__.py +0 -0
  30. coderag/embeddings/base.py +53 -0
  31. coderag/embeddings/hashing.py +43 -0
  32. coderag/embeddings/pipeline.py +87 -0
  33. coderag/embeddings/registry.py +38 -0
  34. coderag/embeddings/sentence_transformer.py +47 -0
  35. coderag/evaluation/__init__.py +0 -0
  36. coderag/evaluation/datasets.py +36 -0
  37. coderag/evaluation/harness.py +246 -0
  38. coderag/git/__init__.py +0 -0
  39. coderag/git/repo.py +139 -0
  40. coderag/indexing/__init__.py +0 -0
  41. coderag/indexing/ignore.py +101 -0
  42. coderag/indexing/indexer.py +445 -0
  43. coderag/llm/__init__.py +0 -0
  44. coderag/llm/anthropic.py +117 -0
  45. coderag/llm/base.py +60 -0
  46. coderag/llm/null.py +16 -0
  47. coderag/llm/registry.py +17 -0
  48. coderag/localdb.py +100 -0
  49. coderag/mcp_server.py +209 -0
  50. coderag/parsing/__init__.py +0 -0
  51. coderag/parsing/base.py +76 -0
  52. coderag/parsing/python.py +294 -0
  53. coderag/parsing/registry.py +40 -0
  54. coderag/retrieval/__init__.py +0 -0
  55. coderag/retrieval/base.py +60 -0
  56. coderag/retrieval/engine.py +176 -0
  57. coderag/retrieval/fusion.py +41 -0
  58. coderag/retrieval/graph.py +139 -0
  59. coderag/retrieval/lexical.py +57 -0
  60. coderag/retrieval/reranking.py +99 -0
  61. coderag/retrieval/semantic.py +53 -0
  62. coderag/retrieval/symbol.py +91 -0
  63. coderag/security/__init__.py +0 -0
  64. coderag/security/authz.py +35 -0
  65. coderag/security/secrets.py +110 -0
  66. coderag/service.py +160 -0
  67. coderag/telemetry.py +72 -0
  68. coderag_ai-0.3.0.dist-info/METADATA +364 -0
  69. coderag_ai-0.3.0.dist-info/RECORD +73 -0
  70. coderag_ai-0.3.0.dist-info/WHEEL +4 -0
  71. coderag_ai-0.3.0.dist-info/entry_points.txt +3 -0
  72. coderag_ai-0.3.0.dist-info/licenses/LICENSE +202 -0
  73. coderag_ai-0.3.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,227 @@
1
+ """initial schema
2
+
3
+ Revision ID: c8a40feeda41
4
+ Revises:
5
+ Create Date: 2026-09-01 21:50:43.238620
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import pgvector.sqlalchemy
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+ from sqlalchemy.dialects import postgresql
15
+
16
+ revision: str = 'c8a40feeda41'
17
+ down_revision: str | None = None
18
+ branch_labels: str | Sequence[str] | None = None
19
+ depends_on: str | Sequence[str] | None = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('evaluation_runs',
25
+ sa.Column('id', sa.Integer(), nullable=False),
26
+ sa.Column('name', sa.String(length=255), nullable=False),
27
+ sa.Column('dataset', sa.String(length=1024), nullable=False),
28
+ sa.Column('recall_at_1', sa.Float(), nullable=False),
29
+ sa.Column('recall_at_3', sa.Float(), nullable=False),
30
+ sa.Column('recall_at_5', sa.Float(), nullable=False),
31
+ sa.Column('recall_at_10', sa.Float(), nullable=False),
32
+ sa.Column('mrr', sa.Float(), nullable=False),
33
+ sa.Column('avg_retrieved_tokens', sa.Float(), nullable=False),
34
+ sa.Column('avg_context_tokens', sa.Float(), nullable=False),
35
+ sa.Column('avg_retrieval_latency_ms', sa.Float(), nullable=False),
36
+ sa.Column('config', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
37
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
38
+ sa.PrimaryKeyConstraint('id')
39
+ )
40
+ op.create_table('repositories',
41
+ sa.Column('id', sa.Integer(), nullable=False),
42
+ sa.Column('name', sa.String(length=255), nullable=False),
43
+ sa.Column('url', sa.String(length=1024), nullable=True),
44
+ sa.Column('local_path', sa.String(length=1024), nullable=False),
45
+ sa.Column('default_branch', sa.String(length=255), nullable=False),
46
+ sa.Column('indexed_commit_sha', sa.String(length=64), nullable=True),
47
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
48
+ sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
49
+ sa.PrimaryKeyConstraint('id')
50
+ )
51
+ op.create_index(op.f('ix_repositories_name'), 'repositories', ['name'], unique=True)
52
+ op.create_table('indexing_runs',
53
+ sa.Column('id', sa.Integer(), nullable=False),
54
+ sa.Column('repository_id', sa.Integer(), nullable=False),
55
+ sa.Column('mode', sa.String(length=16), nullable=False),
56
+ sa.Column('status', sa.String(length=16), nullable=False),
57
+ sa.Column('from_commit', sa.String(length=64), nullable=True),
58
+ sa.Column('to_commit', sa.String(length=64), nullable=True),
59
+ sa.Column('files_indexed', sa.Integer(), nullable=False),
60
+ sa.Column('files_deleted', sa.Integer(), nullable=False),
61
+ sa.Column('symbols_indexed', sa.Integer(), nullable=False),
62
+ sa.Column('symbols_deleted', sa.Integer(), nullable=False),
63
+ sa.Column('embeddings_created', sa.Integer(), nullable=False),
64
+ sa.Column('duration_seconds', sa.Float(), nullable=False),
65
+ sa.Column('error', sa.Text(), nullable=True),
66
+ sa.Column('started_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
67
+ sa.Column('finished_at', sa.DateTime(), nullable=True),
68
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
69
+ sa.PrimaryKeyConstraint('id')
70
+ )
71
+ op.create_table('queries',
72
+ sa.Column('id', sa.Integer(), nullable=False),
73
+ sa.Column('repository_id', sa.Integer(), nullable=False),
74
+ sa.Column('query_text', sa.Text(), nullable=False),
75
+ sa.Column('mode', sa.String(length=16), nullable=False),
76
+ sa.Column('candidates_found', sa.Integer(), nullable=False),
77
+ sa.Column('candidates_selected', sa.Integer(), nullable=False),
78
+ sa.Column('candidate_tokens', sa.Integer(), nullable=False),
79
+ sa.Column('context_tokens', sa.Integer(), nullable=False),
80
+ sa.Column('dropped_tokens', sa.Integer(), nullable=False),
81
+ sa.Column('retrieval_latency_ms', sa.Float(), nullable=False),
82
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
83
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
84
+ sa.PrimaryKeyConstraint('id')
85
+ )
86
+ op.create_table('source_files',
87
+ sa.Column('id', sa.Integer(), nullable=False),
88
+ sa.Column('repository_id', sa.Integer(), nullable=False),
89
+ sa.Column('path', sa.String(length=1024), nullable=False),
90
+ sa.Column('language', sa.String(length=32), nullable=False),
91
+ sa.Column('content_hash', sa.String(length=64), nullable=False),
92
+ sa.Column('size_bytes', sa.Integer(), nullable=False),
93
+ sa.Column('indexed_commit_sha', sa.String(length=64), nullable=True),
94
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
95
+ sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
96
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
97
+ sa.PrimaryKeyConstraint('id'),
98
+ sa.UniqueConstraint('repository_id', 'path', name='uq_source_files_repo_path')
99
+ )
100
+ op.create_index('ix_source_files_repo', 'source_files', ['repository_id'], unique=False)
101
+ op.create_table('llm_requests',
102
+ sa.Column('id', sa.Integer(), nullable=False),
103
+ sa.Column('query_id', sa.Integer(), nullable=True),
104
+ sa.Column('provider', sa.String(length=64), nullable=False),
105
+ sa.Column('model', sa.String(length=128), nullable=False),
106
+ sa.Column('input_tokens', sa.Integer(), nullable=False),
107
+ sa.Column('output_tokens', sa.Integer(), nullable=False),
108
+ sa.Column('cached_input_tokens', sa.Integer(), nullable=True),
109
+ sa.Column('latency_ms', sa.Float(), nullable=False),
110
+ sa.Column('success', sa.Boolean(), nullable=False),
111
+ sa.Column('error', sa.Text(), nullable=True),
112
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
113
+ sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='SET NULL'),
114
+ sa.PrimaryKeyConstraint('id')
115
+ )
116
+ op.create_table('symbols',
117
+ sa.Column('id', sa.Integer(), nullable=False),
118
+ sa.Column('repository_id', sa.Integer(), nullable=False),
119
+ sa.Column('source_file_id', sa.Integer(), nullable=False),
120
+ sa.Column('file_path', sa.String(length=1024), nullable=False),
121
+ sa.Column('language', sa.String(length=32), nullable=False),
122
+ sa.Column('symbol_name', sa.String(length=512), nullable=False),
123
+ sa.Column('qualified_name', sa.String(length=1024), nullable=False),
124
+ sa.Column('symbol_type', sa.String(length=32), nullable=False),
125
+ sa.Column('signature', sa.Text(), nullable=True),
126
+ sa.Column('start_line', sa.Integer(), nullable=False),
127
+ sa.Column('end_line', sa.Integer(), nullable=False),
128
+ sa.Column('parent_symbol_id', sa.Integer(), nullable=True),
129
+ sa.Column('source_code', sa.Text(), nullable=False),
130
+ sa.Column('docstring', sa.Text(), nullable=True),
131
+ sa.Column('source_hash', sa.String(length=64), nullable=False),
132
+ sa.Column('commit_sha', sa.String(length=64), nullable=True),
133
+ sa.Column('token_count', sa.Integer(), nullable=False),
134
+ sa.Column('search_document', sa.Text(), nullable=False),
135
+ sa.Column('fts', postgresql.TSVECTOR(), sa.Computed("to_tsvector('english', search_document)", persisted=True), nullable=False),
136
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
137
+ sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
138
+ sa.ForeignKeyConstraint(['parent_symbol_id'], ['symbols.id'], ondelete='CASCADE'),
139
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
140
+ sa.ForeignKeyConstraint(['source_file_id'], ['source_files.id'], ondelete='CASCADE'),
141
+ sa.PrimaryKeyConstraint('id')
142
+ )
143
+ op.create_index('ix_symbols_commit', 'symbols', ['commit_sha'], unique=False)
144
+ op.create_index('ix_symbols_fts', 'symbols', ['fts'], unique=False, postgresql_using='gin')
145
+ op.create_index('ix_symbols_path', 'symbols', ['repository_id', 'file_path'], unique=False)
146
+ op.create_index('ix_symbols_repo', 'symbols', ['repository_id'], unique=False)
147
+ op.create_index('ix_symbols_repo_qualified', 'symbols', ['repository_id', 'qualified_name'], unique=False)
148
+ op.create_index('ix_symbols_repo_type', 'symbols', ['repository_id', 'symbol_type'], unique=False)
149
+ op.create_index('ix_symbols_source_hash', 'symbols', ['source_hash'], unique=False)
150
+ op.create_table('retrieval_results',
151
+ sa.Column('id', sa.Integer(), nullable=False),
152
+ sa.Column('query_id', sa.Integer(), nullable=False),
153
+ sa.Column('symbol_id', sa.Integer(), nullable=False),
154
+ sa.Column('rank', sa.Integer(), nullable=False),
155
+ sa.Column('score', sa.Float(), nullable=False),
156
+ sa.Column('reasons', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
157
+ sa.Column('selected', sa.Boolean(), nullable=False),
158
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
159
+ sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE'),
160
+ sa.ForeignKeyConstraint(['symbol_id'], ['symbols.id'], ondelete='CASCADE'),
161
+ sa.PrimaryKeyConstraint('id')
162
+ )
163
+ op.create_index('ix_retrieval_query', 'retrieval_results', ['query_id'], unique=False)
164
+ op.create_table('symbol_embeddings',
165
+ sa.Column('id', sa.Integer(), nullable=False),
166
+ sa.Column('repository_id', sa.Integer(), nullable=False),
167
+ sa.Column('symbol_id', sa.Integer(), nullable=False),
168
+ sa.Column('source_hash', sa.String(length=64), nullable=False),
169
+ sa.Column('embedding_model', sa.String(length=255), nullable=False),
170
+ sa.Column('embedding_version', sa.String(length=64), nullable=False),
171
+ sa.Column('embedding_dimension', sa.Integer(), nullable=False),
172
+ sa.Column('embedding', pgvector.sqlalchemy.Vector(), nullable=False),
173
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
174
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
175
+ sa.ForeignKeyConstraint(['symbol_id'], ['symbols.id'], ondelete='CASCADE'),
176
+ sa.PrimaryKeyConstraint('id'),
177
+ sa.UniqueConstraint('symbol_id', 'embedding_model', 'embedding_version', name='uq_embedding_symbol_model')
178
+ )
179
+ op.create_index('ix_embedding_repo', 'symbol_embeddings', ['repository_id'], unique=False)
180
+ op.create_table('symbol_relationships',
181
+ sa.Column('id', sa.Integer(), nullable=False),
182
+ sa.Column('repository_id', sa.Integer(), nullable=False),
183
+ sa.Column('source_symbol_id', sa.Integer(), nullable=False),
184
+ sa.Column('target_symbol_id', sa.Integer(), nullable=True),
185
+ sa.Column('target_name', sa.String(length=1024), nullable=True),
186
+ sa.Column('relationship_type', sa.String(length=32), nullable=False),
187
+ sa.Column('confidence', sa.Float(), nullable=False),
188
+ sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
189
+ sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
190
+ sa.ForeignKeyConstraint(['repository_id'], ['repositories.id'], ondelete='CASCADE'),
191
+ sa.ForeignKeyConstraint(['source_symbol_id'], ['symbols.id'], ondelete='CASCADE'),
192
+ sa.ForeignKeyConstraint(['target_symbol_id'], ['symbols.id'], ondelete='CASCADE'),
193
+ sa.PrimaryKeyConstraint('id')
194
+ )
195
+ op.create_index('ix_rel_repo_source', 'symbol_relationships', ['repository_id', 'source_symbol_id'], unique=False)
196
+ op.create_index('ix_rel_repo_target', 'symbol_relationships', ['repository_id', 'target_symbol_id'], unique=False)
197
+ op.create_index('ix_rel_type', 'symbol_relationships', ['relationship_type'], unique=False)
198
+ # ### end Alembic commands ###
199
+
200
+
201
+ def downgrade() -> None:
202
+ # ### commands auto generated by Alembic - please adjust! ###
203
+ op.drop_index('ix_rel_type', table_name='symbol_relationships')
204
+ op.drop_index('ix_rel_repo_target', table_name='symbol_relationships')
205
+ op.drop_index('ix_rel_repo_source', table_name='symbol_relationships')
206
+ op.drop_table('symbol_relationships')
207
+ op.drop_index('ix_embedding_repo', table_name='symbol_embeddings')
208
+ op.drop_table('symbol_embeddings')
209
+ op.drop_index('ix_retrieval_query', table_name='retrieval_results')
210
+ op.drop_table('retrieval_results')
211
+ op.drop_index('ix_symbols_source_hash', table_name='symbols')
212
+ op.drop_index('ix_symbols_repo_type', table_name='symbols')
213
+ op.drop_index('ix_symbols_repo_qualified', table_name='symbols')
214
+ op.drop_index('ix_symbols_repo', table_name='symbols')
215
+ op.drop_index('ix_symbols_path', table_name='symbols')
216
+ op.drop_index('ix_symbols_fts', table_name='symbols', postgresql_using='gin')
217
+ op.drop_index('ix_symbols_commit', table_name='symbols')
218
+ op.drop_table('symbols')
219
+ op.drop_table('llm_requests')
220
+ op.drop_index('ix_source_files_repo', table_name='source_files')
221
+ op.drop_table('source_files')
222
+ op.drop_table('queries')
223
+ op.drop_table('indexing_runs')
224
+ op.drop_index(op.f('ix_repositories_name'), table_name='repositories')
225
+ op.drop_table('repositories')
226
+ op.drop_table('evaluation_runs')
227
+ # ### end Alembic commands ###
coderag/db/models.py ADDED
@@ -0,0 +1,275 @@
1
+ """SQLAlchemy 2.0 ORM models — the persisted code index and telemetry.
2
+
3
+ Design notes:
4
+ * Every retrieval-relevant table carries ``repository_id`` so queries can
5
+ (and must) be scoped to a single repository — see ``security/authz.py``.
6
+ * ``symbols.fts`` is a generated ``tsvector`` (Postgres full-text search).
7
+ * ``symbol_embeddings.embedding`` uses a dimension-less pgvector column so
8
+ the embedding model (and its dimension) can change without a schema
9
+ migration; the concrete dimension is recorded per row.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import datetime as dt
15
+
16
+ from pgvector.sqlalchemy import Vector
17
+ from sqlalchemy import (
18
+ JSON,
19
+ Boolean,
20
+ Computed,
21
+ Float,
22
+ ForeignKey,
23
+ Index,
24
+ Integer,
25
+ String,
26
+ Text,
27
+ UniqueConstraint,
28
+ func,
29
+ )
30
+ from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
31
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
32
+
33
+
34
+ class Base(DeclarativeBase):
35
+ type_annotation_map = {dict: JSON}
36
+
37
+
38
+ TS = dt.datetime
39
+
40
+
41
+ class Repository(Base):
42
+ __tablename__ = "repositories"
43
+
44
+ id: Mapped[int] = mapped_column(primary_key=True)
45
+ name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
46
+ url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
47
+ local_path: Mapped[str] = mapped_column(String(1024))
48
+ default_branch: Mapped[str] = mapped_column(String(255), default="main")
49
+ indexed_commit_sha: Mapped[str | None] = mapped_column(String(64), nullable=True)
50
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
51
+ updated_at: Mapped[TS] = mapped_column(server_default=func.now(), onupdate=func.now())
52
+
53
+
54
+ class SourceFile(Base):
55
+ __tablename__ = "source_files"
56
+ __table_args__ = (
57
+ UniqueConstraint("repository_id", "path", name="uq_source_files_repo_path"),
58
+ Index("ix_source_files_repo", "repository_id"),
59
+ )
60
+
61
+ id: Mapped[int] = mapped_column(primary_key=True)
62
+ repository_id: Mapped[int] = mapped_column(
63
+ ForeignKey("repositories.id", ondelete="CASCADE")
64
+ )
65
+ path: Mapped[str] = mapped_column(String(1024))
66
+ language: Mapped[str] = mapped_column(String(32))
67
+ content_hash: Mapped[str] = mapped_column(String(64))
68
+ size_bytes: Mapped[int] = mapped_column(Integer, default=0)
69
+ # Whole-file token count, recorded at index time so the "what would reading
70
+ # this file have cost?" baseline needs no file I/O at query time.
71
+ token_count: Mapped[int] = mapped_column(Integer, default=0)
72
+ indexed_commit_sha: Mapped[str | None] = mapped_column(String(64), nullable=True)
73
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
74
+ updated_at: Mapped[TS] = mapped_column(server_default=func.now(), onupdate=func.now())
75
+
76
+
77
+ class Symbol(Base):
78
+ __tablename__ = "symbols"
79
+ __table_args__ = (
80
+ Index("ix_symbols_repo", "repository_id"),
81
+ Index("ix_symbols_repo_qualified", "repository_id", "qualified_name"),
82
+ Index("ix_symbols_repo_type", "repository_id", "symbol_type"),
83
+ Index("ix_symbols_path", "repository_id", "file_path"),
84
+ Index("ix_symbols_source_hash", "source_hash"),
85
+ Index("ix_symbols_commit", "commit_sha"),
86
+ Index("ix_symbols_fts", "fts", postgresql_using="gin"),
87
+ )
88
+
89
+ id: Mapped[int] = mapped_column(primary_key=True)
90
+ repository_id: Mapped[int] = mapped_column(
91
+ ForeignKey("repositories.id", ondelete="CASCADE")
92
+ )
93
+ source_file_id: Mapped[int] = mapped_column(
94
+ ForeignKey("source_files.id", ondelete="CASCADE")
95
+ )
96
+ file_path: Mapped[str] = mapped_column(String(1024))
97
+ language: Mapped[str] = mapped_column(String(32))
98
+
99
+ symbol_name: Mapped[str] = mapped_column(String(512))
100
+ qualified_name: Mapped[str] = mapped_column(String(1024))
101
+ symbol_type: Mapped[str] = mapped_column(String(32)) # module|class|function|method
102
+ signature: Mapped[str | None] = mapped_column(Text, nullable=True)
103
+
104
+ start_line: Mapped[int] = mapped_column(Integer)
105
+ end_line: Mapped[int] = mapped_column(Integer)
106
+ parent_symbol_id: Mapped[int | None] = mapped_column(
107
+ ForeignKey("symbols.id", ondelete="CASCADE"), nullable=True
108
+ )
109
+
110
+ source_code: Mapped[str] = mapped_column(Text)
111
+ docstring: Mapped[str | None] = mapped_column(Text, nullable=True)
112
+ source_hash: Mapped[str] = mapped_column(String(64))
113
+ commit_sha: Mapped[str | None] = mapped_column(String(64), nullable=True)
114
+ token_count: Mapped[int] = mapped_column(Integer, default=0)
115
+
116
+ # Text feeding full-text search (identifiers + signature + docstring).
117
+ search_document: Mapped[str] = mapped_column(Text, default="")
118
+ fts: Mapped[str] = mapped_column(
119
+ TSVECTOR,
120
+ Computed("to_tsvector('english', search_document)", persisted=True),
121
+ )
122
+
123
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
124
+ updated_at: Mapped[TS] = mapped_column(server_default=func.now(), onupdate=func.now())
125
+
126
+ parent: Mapped[Symbol | None] = relationship(
127
+ "Symbol", remote_side="Symbol.id", back_populates="children"
128
+ )
129
+ children: Mapped[list[Symbol]] = relationship(
130
+ "Symbol", back_populates="parent", viewonly=True
131
+ )
132
+
133
+
134
+ class SymbolRelationship(Base):
135
+ __tablename__ = "symbol_relationships"
136
+ __table_args__ = (
137
+ Index("ix_rel_repo_source", "repository_id", "source_symbol_id"),
138
+ Index("ix_rel_repo_target", "repository_id", "target_symbol_id"),
139
+ Index("ix_rel_type", "relationship_type"),
140
+ )
141
+
142
+ id: Mapped[int] = mapped_column(primary_key=True)
143
+ repository_id: Mapped[int] = mapped_column(
144
+ ForeignKey("repositories.id", ondelete="CASCADE")
145
+ )
146
+ source_symbol_id: Mapped[int] = mapped_column(
147
+ ForeignKey("symbols.id", ondelete="CASCADE")
148
+ )
149
+ # Target may be unresolved (an external/未-indexed name), so it is nullable
150
+ # and we also keep the raw target_name.
151
+ target_symbol_id: Mapped[int | None] = mapped_column(
152
+ ForeignKey("symbols.id", ondelete="CASCADE"), nullable=True
153
+ )
154
+ target_name: Mapped[str | None] = mapped_column(String(1024), nullable=True)
155
+ relationship_type: Mapped[str] = mapped_column(String(32))
156
+ confidence: Mapped[float] = mapped_column(Float, default=1.0)
157
+ meta: Mapped[dict | None] = mapped_column("metadata", JSONB, nullable=True)
158
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
159
+
160
+
161
+ class SymbolEmbedding(Base):
162
+ __tablename__ = "symbol_embeddings"
163
+ __table_args__ = (
164
+ UniqueConstraint(
165
+ "symbol_id", "embedding_model", "embedding_version",
166
+ name="uq_embedding_symbol_model",
167
+ ),
168
+ Index("ix_embedding_repo", "repository_id"),
169
+ )
170
+
171
+ id: Mapped[int] = mapped_column(primary_key=True)
172
+ repository_id: Mapped[int] = mapped_column(
173
+ ForeignKey("repositories.id", ondelete="CASCADE")
174
+ )
175
+ symbol_id: Mapped[int] = mapped_column(ForeignKey("symbols.id", ondelete="CASCADE"))
176
+ source_hash: Mapped[str] = mapped_column(String(64))
177
+ embedding_model: Mapped[str] = mapped_column(String(255))
178
+ embedding_version: Mapped[str] = mapped_column(String(64))
179
+ embedding_dimension: Mapped[int] = mapped_column(Integer)
180
+ embedding: Mapped[list[float]] = mapped_column(Vector()) # dimension-less
181
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
182
+
183
+
184
+ class IndexingRun(Base):
185
+ __tablename__ = "indexing_runs"
186
+
187
+ id: Mapped[int] = mapped_column(primary_key=True)
188
+ repository_id: Mapped[int] = mapped_column(
189
+ ForeignKey("repositories.id", ondelete="CASCADE")
190
+ )
191
+ mode: Mapped[str] = mapped_column(String(16)) # full|incremental
192
+ status: Mapped[str] = mapped_column(String(16), default="running")
193
+ from_commit: Mapped[str | None] = mapped_column(String(64), nullable=True)
194
+ to_commit: Mapped[str | None] = mapped_column(String(64), nullable=True)
195
+ files_indexed: Mapped[int] = mapped_column(Integer, default=0)
196
+ files_deleted: Mapped[int] = mapped_column(Integer, default=0)
197
+ symbols_indexed: Mapped[int] = mapped_column(Integer, default=0)
198
+ symbols_deleted: Mapped[int] = mapped_column(Integer, default=0)
199
+ embeddings_created: Mapped[int] = mapped_column(Integer, default=0)
200
+ duration_seconds: Mapped[float] = mapped_column(Float, default=0.0)
201
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
202
+ started_at: Mapped[TS] = mapped_column(server_default=func.now())
203
+ finished_at: Mapped[TS | None] = mapped_column(nullable=True)
204
+
205
+
206
+ class QueryRecord(Base):
207
+ __tablename__ = "queries"
208
+
209
+ id: Mapped[int] = mapped_column(primary_key=True)
210
+ repository_id: Mapped[int] = mapped_column(
211
+ ForeignKey("repositories.id", ondelete="CASCADE")
212
+ )
213
+ query_text: Mapped[str] = mapped_column(Text)
214
+ mode: Mapped[str] = mapped_column(String(16)) # search|context|ask
215
+ candidates_found: Mapped[int] = mapped_column(Integer, default=0)
216
+ candidates_selected: Mapped[int] = mapped_column(Integer, default=0)
217
+ candidate_tokens: Mapped[int] = mapped_column(Integer, default=0)
218
+ context_tokens: Mapped[int] = mapped_column(Integer, default=0)
219
+ dropped_tokens: Mapped[int] = mapped_column(Integer, default=0)
220
+ # Counterfactual: tokens it would have cost to read the whole files that the
221
+ # selected symbols came from (the "just open the files" baseline).
222
+ baseline_tokens: Mapped[int] = mapped_column(Integer, default=0)
223
+ baseline_files: Mapped[int] = mapped_column(Integer, default=0)
224
+ retrieval_latency_ms: Mapped[float] = mapped_column(Float, default=0.0)
225
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
226
+
227
+
228
+ class RetrievalResult(Base):
229
+ __tablename__ = "retrieval_results"
230
+ __table_args__ = (Index("ix_retrieval_query", "query_id"),)
231
+
232
+ id: Mapped[int] = mapped_column(primary_key=True)
233
+ query_id: Mapped[int] = mapped_column(ForeignKey("queries.id", ondelete="CASCADE"))
234
+ symbol_id: Mapped[int] = mapped_column(ForeignKey("symbols.id", ondelete="CASCADE"))
235
+ rank: Mapped[int] = mapped_column(Integer)
236
+ score: Mapped[float] = mapped_column(Float)
237
+ reasons: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
238
+ selected: Mapped[bool] = mapped_column(Boolean, default=False)
239
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
240
+
241
+
242
+ class LLMRequest(Base):
243
+ __tablename__ = "llm_requests"
244
+
245
+ id: Mapped[int] = mapped_column(primary_key=True)
246
+ query_id: Mapped[int | None] = mapped_column(
247
+ ForeignKey("queries.id", ondelete="SET NULL"), nullable=True
248
+ )
249
+ provider: Mapped[str] = mapped_column(String(64))
250
+ model: Mapped[str] = mapped_column(String(128))
251
+ input_tokens: Mapped[int] = mapped_column(Integer, default=0)
252
+ output_tokens: Mapped[int] = mapped_column(Integer, default=0)
253
+ cached_input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
254
+ latency_ms: Mapped[float] = mapped_column(Float, default=0.0)
255
+ success: Mapped[bool] = mapped_column(Boolean, default=True)
256
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
257
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
258
+
259
+
260
+ class EvaluationRun(Base):
261
+ __tablename__ = "evaluation_runs"
262
+
263
+ id: Mapped[int] = mapped_column(primary_key=True)
264
+ name: Mapped[str] = mapped_column(String(255))
265
+ dataset: Mapped[str] = mapped_column(String(1024))
266
+ recall_at_1: Mapped[float] = mapped_column(Float, default=0.0)
267
+ recall_at_3: Mapped[float] = mapped_column(Float, default=0.0)
268
+ recall_at_5: Mapped[float] = mapped_column(Float, default=0.0)
269
+ recall_at_10: Mapped[float] = mapped_column(Float, default=0.0)
270
+ mrr: Mapped[float] = mapped_column(Float, default=0.0)
271
+ avg_retrieved_tokens: Mapped[float] = mapped_column(Float, default=0.0)
272
+ avg_context_tokens: Mapped[float] = mapped_column(Float, default=0.0)
273
+ avg_retrieval_latency_ms: Mapped[float] = mapped_column(Float, default=0.0)
274
+ config: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
275
+ created_at: Mapped[TS] = mapped_column(server_default=func.now())
File without changes
@@ -0,0 +1,53 @@
1
+ """Embedding provider abstraction.
2
+
3
+ No source code should need to leave company infrastructure to be embedded, so
4
+ every provider runs locally. The default (``HashingEmbeddingProvider``) is
5
+ deterministic and dependency-free; ``SentenceTransformerEmbeddingProvider`` uses
6
+ a local model. Embeddings are provider-independent: each stored vector records
7
+ its model, version, and dimension so the model can change and code can be
8
+ re-embedded (ADR-005 / schema notes).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC, abstractmethod
14
+
15
+
16
+ class EmbeddingProvider(ABC):
17
+ model_name: str
18
+ model_version: str
19
+ dimension: int
20
+
21
+ @abstractmethod
22
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
23
+ """Embed a batch of documents (structured symbol inputs)."""
24
+
25
+ @abstractmethod
26
+ def embed_query(self, text: str) -> list[float]:
27
+ """Embed a single query string."""
28
+
29
+
30
+ def build_embedding_input(
31
+ repository_name: str,
32
+ file_path: str,
33
+ qualified_name: str,
34
+ symbol_type: str,
35
+ signature: str | None,
36
+ docstring: str | None,
37
+ source_code: str,
38
+ max_code_chars: int = 4000,
39
+ ) -> str:
40
+ """Compose an embedding input that carries structural context (spec §9)."""
41
+ code = source_code if len(source_code) <= max_code_chars else source_code[:max_code_chars]
42
+ parts = [
43
+ f"Repository: {repository_name}",
44
+ f"File: {file_path}",
45
+ f"Symbol: {qualified_name}",
46
+ f"Type: {symbol_type}",
47
+ ]
48
+ if signature:
49
+ parts.append(f"Signature: {signature}")
50
+ if docstring:
51
+ parts.append(f"Documentation: {docstring}")
52
+ parts.append(f"Code:\n{code}")
53
+ return "\n".join(parts)
@@ -0,0 +1,43 @@
1
+ """Deterministic, offline embedding provider (default).
2
+
3
+ Uses signed feature hashing over word-expanded identifiers: each token is hashed
4
+ to a bucket with a +/- sign, summed, and L2-normalised. This is NOT a learned
5
+ semantic model — similarity reflects shared vocabulary — but it is fully offline,
6
+ deterministic, and dependency-free, which makes it ideal for tests, CI, and
7
+ air-gapped dev. For genuine semantic matching use
8
+ ``SentenceTransformerEmbeddingProvider``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import math
15
+
16
+ from coderag.core.text import expand_query_terms
17
+ from coderag.embeddings.base import EmbeddingProvider
18
+
19
+
20
+ class HashingEmbeddingProvider(EmbeddingProvider):
21
+ model_version = "1"
22
+
23
+ def __init__(self, dimension: int = 384) -> None:
24
+ self.dimension = dimension
25
+ self.model_name = f"hashing-{dimension}"
26
+
27
+ def _embed(self, text: str) -> list[float]:
28
+ vec = [0.0] * self.dimension
29
+ for token in expand_query_terms(text):
30
+ digest = hashlib.md5(token.lower().encode("utf-8")).digest() # noqa: S324
31
+ idx = int.from_bytes(digest[:4], "big") % self.dimension
32
+ sign = 1.0 if (digest[4] & 1) else -1.0
33
+ vec[idx] += sign
34
+ norm = math.sqrt(sum(v * v for v in vec))
35
+ if norm > 0:
36
+ vec = [v / norm for v in vec]
37
+ return vec
38
+
39
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
40
+ return [self._embed(t) for t in texts]
41
+
42
+ def embed_query(self, text: str) -> list[float]:
43
+ return self._embed(text)