coderag-ai 0.3.2__tar.gz → 0.3.4__tar.gz

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 (140) hide show
  1. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/PKG-INFO +1 -1
  2. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/pyproject.toml +1 -1
  3. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/api/app.py +8 -0
  4. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/api/dashboard.py +5 -0
  5. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/api/schemas.py +6 -0
  6. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/cli/main.py +33 -12
  7. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/core/config.py +7 -0
  8. coderag_ai-0.3.4/src/coderag/db/migrate.py +51 -0
  9. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase13_measure.py +25 -0
  10. coderag_ai-0.3.4/tests/test_phase15_migrate.py +43 -0
  11. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/.env.example +0 -0
  12. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/.gitignore +0 -0
  13. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/CODE_OF_CONDUCT.md +0 -0
  14. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/CONTRIBUTING.md +0 -0
  15. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/Dockerfile +0 -0
  16. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/LICENSE +0 -0
  17. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/Makefile +0 -0
  18. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/NOTICE +0 -0
  19. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/README.md +0 -0
  20. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/SECURITY.md +0 -0
  21. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/alembic.ini +0 -0
  22. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docker/entrypoint.sh +0 -0
  23. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docker-compose.yml +0 -0
  24. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adding-language.md +0 -0
  25. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/001-postgres-pgvector.md +0 -0
  26. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/002-structural-chunks.md +0 -0
  27. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/003-hybrid-search.md +0 -0
  28. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/004-tree-sitter.md +0 -0
  29. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/005-llm-provider-independence.md +0 -0
  30. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/006-repository-authorization.md +0 -0
  31. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/adr/007-token-budgeting.md +0 -0
  32. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/architecture.md +0 -0
  33. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/claude-code.md +0 -0
  34. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/evaluation.md +0 -0
  35. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/install-macos.md +0 -0
  36. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/licenses.md +0 -0
  37. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/llm-providers.md +0 -0
  38. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/retrieval.md +0 -0
  39. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/security.md +0 -0
  40. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/docs/vscode-extension.md +0 -0
  41. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/__init__.py +0 -0
  42. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/api.py +0 -0
  43. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/auth.py +0 -0
  44. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/checkout_service.py +0 -0
  45. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/models.py +0 -0
  46. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/payment_repository.py +0 -0
  47. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/payment_service.py +0 -0
  48. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/retry_policy.py +0 -0
  49. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/payments/users.py +0 -0
  50. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/tests/test_payment_service.py +0 -0
  51. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/demo-repository/tests/test_retry_policy.py +0 -0
  52. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/examples/eval/eval_dataset.json +0 -0
  53. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/scripts/gen_initial_migration.py +0 -0
  54. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/__init__.py +0 -0
  55. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/analyzers/__init__.py +0 -0
  56. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/analyzers/base.py +0 -0
  57. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/analyzers/flake8_adapter.py +0 -0
  58. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/analyzers/pylint_adapter.py +0 -0
  59. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/analyzers/workflow.py +0 -0
  60. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/api/__init__.py +0 -0
  61. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/cli/__init__.py +0 -0
  62. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/context/__init__.py +0 -0
  63. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/context/builder.py +0 -0
  64. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/context/package.py +0 -0
  65. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/core/__init__.py +0 -0
  66. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/core/logging.py +0 -0
  67. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/core/text.py +0 -0
  68. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/core/tokens.py +0 -0
  69. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/__init__.py +0 -0
  70. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/base.py +0 -0
  71. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/migrations/env.py +0 -0
  72. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/migrations/script.py.mako +0 -0
  73. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/.gitkeep +0 -0
  74. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/9a973932b94d_measure_baseline_tokens.py +0 -0
  75. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/c8a40feeda41_initial_schema.py +0 -0
  76. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/db/models.py +0 -0
  77. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/__init__.py +0 -0
  78. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/base.py +0 -0
  79. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/hashing.py +0 -0
  80. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/pipeline.py +0 -0
  81. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/registry.py +0 -0
  82. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/embeddings/sentence_transformer.py +0 -0
  83. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/evaluation/__init__.py +0 -0
  84. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/evaluation/datasets.py +0 -0
  85. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/evaluation/harness.py +0 -0
  86. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/git/__init__.py +0 -0
  87. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/git/repo.py +0 -0
  88. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/indexing/__init__.py +0 -0
  89. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/indexing/ignore.py +0 -0
  90. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/indexing/indexer.py +0 -0
  91. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/llm/__init__.py +0 -0
  92. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/llm/anthropic.py +0 -0
  93. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/llm/base.py +0 -0
  94. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/llm/null.py +0 -0
  95. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/llm/registry.py +0 -0
  96. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/localdb.py +0 -0
  97. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/mcp_server.py +0 -0
  98. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/parsing/__init__.py +0 -0
  99. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/parsing/base.py +0 -0
  100. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/parsing/python.py +0 -0
  101. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/parsing/registry.py +0 -0
  102. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/__init__.py +0 -0
  103. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/base.py +0 -0
  104. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/engine.py +0 -0
  105. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/fusion.py +0 -0
  106. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/graph.py +0 -0
  107. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/lexical.py +0 -0
  108. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/reranking.py +0 -0
  109. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/semantic.py +0 -0
  110. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/retrieval/symbol.py +0 -0
  111. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/security/__init__.py +0 -0
  112. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/security/authz.py +0 -0
  113. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/security/secrets.py +0 -0
  114. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/service.py +0 -0
  115. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/src/coderag/telemetry.py +0 -0
  116. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/__init__.py +0 -0
  117. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/conftest.py +0 -0
  118. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase0_db.py +0 -0
  119. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase0_migrations.py +0 -0
  120. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase10_analyzers.py +0 -0
  121. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase11_dashboard.py +0 -0
  122. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase12_mcp.py +0 -0
  123. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase14_localdb.py +0 -0
  124. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase1_indexer.py +0 -0
  125. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase1_parser.py +0 -0
  126. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase2_cli.py +0 -0
  127. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase2_retrieval.py +0 -0
  128. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase3_embeddings.py +0 -0
  129. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase3_semantic.py +0 -0
  130. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase3_sentence_transformer.py +0 -0
  131. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase4_graph.py +0 -0
  132. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase5_context_db.py +0 -0
  133. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase5_context_unit.py +0 -0
  134. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase6_api.py +0 -0
  135. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase6_ask.py +0 -0
  136. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase6_llm.py +0 -0
  137. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase7_eval.py +0 -0
  138. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase8_incremental.py +0 -0
  139. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_phase9_rerank.py +0 -0
  140. {coderag_ai-0.3.2 → coderag_ai-0.3.4}/tests/test_security_secrets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderag-ai
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Token-efficient, structure-aware Code Intelligence + RAG for private source repositories.
5
5
  Project-URL: Homepage, https://github.com/galacticsurfer/coderag
6
6
  Project-URL: Repository, https://github.com/galacticsurfer/coderag
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
7
7
  # command are both `coderag`. Plain "coderag" is taken on PyPI by an unrelated project,
8
8
  # and "code-rag" is rejected as too similar (PyPI collapses -/_/. when comparing names).
9
9
  name = "coderag-ai"
10
- version = "0.3.2"
10
+ version = "0.3.4"
11
11
  description = "Token-efficient, structure-aware Code Intelligence + RAG for private source repositories."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
@@ -286,6 +286,10 @@ def metrics(session: Session = Depends(get_session)) -> s.MetricsOut:
286
286
  ) or 0)
287
287
  saved_vs_files = base_sum - base_ctx
288
288
  reduction_vs_files = round(100.0 * saved_vs_files / base_sum, 1) if base_sum else 0.0
289
+ from coderag.core.config import get_settings
290
+
291
+ _s = get_settings()
292
+ pin, pout = _s.price_input_per_mtok, _s.price_output_per_mtok
289
293
  return s.MetricsOut(
290
294
  repositories=count(Repository), symbols=count(Symbol),
291
295
  embeddings=count(SymbolEmbedding), relationships=count(SymbolRelationship),
@@ -297,6 +301,10 @@ def metrics(session: Session = Depends(get_session)) -> s.MetricsOut:
297
301
  total_tokens_saved=saved, avg_token_reduction_percent=reduction,
298
302
  total_baseline_tokens=base_sum, total_saved_vs_files=saved_vs_files,
299
303
  reduction_vs_files_percent=reduction_vs_files,
304
+ price_input_per_mtok=pin, price_output_per_mtok=pout,
305
+ cost_saved_vs_files_usd=round(saved_vs_files / 1e6 * pin, 4),
306
+ cost_context_sent_usd=round(int(ctx_sum) / 1e6 * pin, 4),
307
+ cost_llm_usd=round(int(in_tok) / 1e6 * pin + int(out_tok) / 1e6 * pout, 4),
300
308
  )
301
309
 
302
310
 
@@ -109,6 +109,7 @@ DASHBOARD_HTML = r"""<!doctype html>
109
109
  </header>
110
110
 
111
111
  <section class="kpis" id="kpis"></section>
112
+ <p style="font-size:12px;color:var(--muted);margin:-14px 0 22px">Dollar figures are estimates at the configured per-million prices (CODERAG_PRICE_INPUT_PER_MTOK / _OUTPUT_PER_MTOK) — not billing data. On a flat-rate Claude plan, savings show up as headroom rather than a refund.</p>
112
113
 
113
114
  <section class="card">
114
115
  <h2>Context vs. saved — recent queries</h2>
@@ -143,6 +144,7 @@ DASHBOARD_HTML = r"""<!doctype html>
143
144
  <script>
144
145
  const n = x => (x==null? "—" : Number(x).toLocaleString());
145
146
  const pct = x => (x==null? "—" : Number(x).toFixed(1) + "%");
147
+ const usd = x => (x==null? "—" : "$" + Number(x).toLocaleString(undefined,{minimumFractionDigits:2, maximumFractionDigits:2}));
146
148
  const esc = s => (s||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
147
149
 
148
150
  function tile(label, val, unit, hero){
@@ -153,7 +155,10 @@ function tile(label, val, unit, hero){
153
155
  function renderKpis(m){
154
156
  document.getElementById('kpis').innerHTML =
155
157
  tile('Saved vs reading files', n(m.total_saved_vs_files), 'tok', true) +
158
+ tile('Est. $ saved', usd(m.cost_saved_vs_files_usd), '', true) +
156
159
  tile('Reduction vs files', pct(m.reduction_vs_files_percent), '', true) +
160
+ tile('Est. $ context sent', usd(m.cost_context_sent_usd)) +
161
+ tile('Est. $ LLM spend', usd(m.cost_llm_usd)) +
157
162
  tile('Whole-file baseline', n(m.total_baseline_tokens), 'tok') +
158
163
  tile('Tokens saved (budgeting)', n(m.total_tokens_saved)) +
159
164
  tile('Overall reduction', pct(m.avg_token_reduction_percent)) +
@@ -158,6 +158,12 @@ class MetricsOut(BaseModel):
158
158
  total_baseline_tokens: int
159
159
  total_saved_vs_files: int
160
160
  reduction_vs_files_percent: float
161
+ # Indicative USD at the configured per-million prices (estimates, not billing)
162
+ price_input_per_mtok: float
163
+ price_output_per_mtok: float
164
+ cost_saved_vs_files_usd: float
165
+ cost_context_sent_usd: float
166
+ cost_llm_usd: float
161
167
 
162
168
 
163
169
  class QueryRow(BaseModel):
@@ -45,19 +45,17 @@ def localdb_start(
45
45
  raise typer.Exit(1) from None
46
46
  console.print(f"[green]PostgreSQL running.[/]\n\n export CODERAG_DATABASE_URL='{url}'\n")
47
47
  if migrate:
48
- import os
49
- import subprocess
48
+ # Uses Alembic's Python API against the packaged migrations — an installed
49
+ # wheel has neither the `alembic` console script on PATH nor alembic.ini.
50
+ from coderag.db.migrate import upgrade_to_head
50
51
 
51
- env = dict(os.environ, CODERAG_DATABASE_URL=url)
52
- r = subprocess.run(["alembic", "upgrade", "head"], env=env,
53
- capture_output=True, text=True)
54
- if r.returncode == 0:
52
+ try:
53
+ upgrade_to_head(url)
55
54
  console.print("[green]Migrations applied.[/]")
56
- else:
57
- console.print(
58
- "[yellow]Could not run migrations automatically "
59
- "(run `alembic upgrade head` from a repo checkout).[/]"
60
- )
55
+ except Exception as exc:
56
+ console.print(f"[red]Migrations failed:[/] {exc}")
57
+ console.print("[yellow]Retry with:[/] coderag migrate")
58
+ raise typer.Exit(1) from None
61
59
 
62
60
 
63
61
  @localdb_app.command("stop")
@@ -82,6 +80,23 @@ def localdb_status(
82
80
  console.print(f"[green]running (pid {pid})[/]" if pid else "[yellow]not running[/]")
83
81
 
84
82
 
83
+ @app.command()
84
+ def migrate(
85
+ database_url: str | None = typer.Option(
86
+ None, "--database-url", help="Defaults to CODERAG_DATABASE_URL."
87
+ ),
88
+ ) -> None:
89
+ """Create/upgrade the database schema (works from a pip/pipx install)."""
90
+ from coderag.db.migrate import current_revision, upgrade_to_head
91
+
92
+ try:
93
+ url = upgrade_to_head(database_url)
94
+ except Exception as exc:
95
+ console.print(f"[red]Migration failed:[/] {exc}")
96
+ raise typer.Exit(1) from None
97
+ console.print(f"[green]Database is up to date[/] (revision {current_revision(url)}).")
98
+
99
+
85
100
  @app.command()
86
101
  def index(
87
102
  path: str = typer.Argument(..., help="Path to the repository to index."),
@@ -180,9 +195,15 @@ def context(
180
195
  f"{acct.final_prompt_tokens:,}")
181
196
  console.print(t)
182
197
  if acct.baseline_tokens:
198
+ price = get_settings().price_input_per_mtok
199
+ saved_usd = acct.tokens_saved_vs_files / 1e6 * price
183
200
  console.print(
184
201
  f"[green]saved {acct.tokens_saved_vs_files:,} tokens "
185
- f"({acct.reduction_vs_files}%) vs opening those files[/]"
202
+ f"({acct.reduction_vs_files}%) vs opening those files[/] "
203
+ f"[dim]≈ ${saved_usd:.4f} at ${price:.2f}/M input tokens; "
204
+ f"× 1,000 similar queries ≈ ${saved_usd * 1000:.2f}[/]\n"
205
+ f"[dim]Estimate at configured prices, not billing data. On a "
206
+ f"flat-rate plan this is headroom, not a refund.[/]"
186
207
  )
187
208
  else:
188
209
  console.print("[yellow]No baseline recorded (nothing selected).[/]")
@@ -85,6 +85,13 @@ class Settings(BaseSettings):
85
85
  llm_max_output_tokens: int = 1024
86
86
  llm_timeout_seconds: float = 60.0
87
87
 
88
+ # ---- Cost estimation -------------------------------------------------
89
+ # USD per million tokens, used only to turn token counts into indicative
90
+ # dollar figures. Defaults are Claude Opus 4.8 list prices; override for your
91
+ # model/plan. These are ESTIMATES at configured prices — not billing data.
92
+ price_input_per_mtok: float = 5.00
93
+ price_output_per_mtok: float = 25.00
94
+
88
95
  # ---- Analyzers (Phase 10) ------------------------------------------
89
96
  max_fix_attempts: int = 2
90
97
 
@@ -0,0 +1,51 @@
1
+ """Run database migrations from an installed package.
2
+
3
+ The `alembic` console script and `alembic.ini` are only available in a source
4
+ checkout. When CodeRAG is installed as a wheel (pipx/pip), neither is on PATH, so
5
+ migrations are driven through Alembic's Python API with the migration directory
6
+ resolved from the installed package.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ from coderag.core.config import get_settings
14
+
15
+
16
+ def migrations_path() -> Path:
17
+ """Filesystem path of the packaged Alembic migration directory."""
18
+ return Path(__file__).resolve().parent / "migrations"
19
+
20
+
21
+ def _config(url: str):
22
+ from alembic.config import Config
23
+
24
+ cfg = Config()
25
+ cfg.set_main_option("script_location", str(migrations_path()))
26
+ # Escape '%' so ConfigParser interpolation doesn't choke on URL-encoded values.
27
+ cfg.set_main_option("sqlalchemy.url", url.replace("%", "%%"))
28
+ return cfg
29
+
30
+
31
+ def upgrade_to_head(url: str | None = None) -> str:
32
+ """Upgrade the database to the latest revision. Returns the URL used."""
33
+ from alembic import command
34
+
35
+ target = url or get_settings().database_url
36
+ command.upgrade(_config(target), "head")
37
+ return target
38
+
39
+
40
+ def current_revision(url: str | None = None) -> str | None:
41
+ """Return the revision the database is currently stamped at (None if empty)."""
42
+ from alembic.migration import MigrationContext
43
+ from sqlalchemy import create_engine
44
+
45
+ target = url or get_settings().database_url
46
+ engine = create_engine(target)
47
+ try:
48
+ with engine.connect() as conn:
49
+ return MigrationContext.configure(conn).get_current_revision()
50
+ finally:
51
+ engine.dispose()
@@ -77,3 +77,28 @@ def test_mcp_context_includes_measurement(db_session, demo_repo):
77
77
  out = coderag_context("retry failed payment", repository="payments")
78
78
  assert "fewer input tokens" in out["measurement"]
79
79
  assert out["token_accounting"]["baseline_tokens"] > 0
80
+
81
+
82
+ def test_metrics_exposes_dollar_estimates(db_session, demo_repo):
83
+ """Token savings are convertible to indicative USD at configured prices."""
84
+ from coderag.core.config import get_settings
85
+
86
+ run_context(db_session, "retry failed payment", "payments")
87
+ db_session.commit()
88
+
89
+ client = TestClient(app)
90
+ m = client.get("/metrics").json()
91
+ s = get_settings()
92
+ assert m["price_input_per_mtok"] == s.price_input_per_mtok
93
+ # dollars must be a faithful function of tokens and the configured price
94
+ expected = round(m["total_saved_vs_files"] / 1e6 * s.price_input_per_mtok, 4)
95
+ assert m["cost_saved_vs_files_usd"] == expected
96
+ assert m["cost_context_sent_usd"] >= 0.0
97
+ assert m["cost_llm_usd"] >= 0.0
98
+
99
+
100
+ def test_price_settings_are_configurable():
101
+ from coderag.core.config import Settings
102
+
103
+ s = Settings(price_input_per_mtok=3.0, price_output_per_mtok=15.0)
104
+ assert s.price_input_per_mtok == 3.0 and s.price_output_per_mtok == 15.0
@@ -0,0 +1,43 @@
1
+ """Migrations must work from an installed wheel (no alembic.ini, no CLI on PATH)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ import pytest
8
+ from sqlalchemy import create_engine, text
9
+
10
+ from coderag.db.migrate import current_revision, migrations_path, upgrade_to_head
11
+
12
+ pytestmark = pytest.mark.db
13
+
14
+
15
+ def test_migrations_are_packaged():
16
+ """The migration scripts ship inside the package, not just the source tree."""
17
+ path = migrations_path()
18
+ assert (path / "env.py").is_file()
19
+ versions = list((path / "versions").glob("*.py"))
20
+ assert versions, "no migration revisions packaged"
21
+
22
+
23
+ def test_upgrade_to_head_without_alembic_ini(pg_uri: str):
24
+ """Regression: `localdb start` used to shell out to an `alembic` binary that
25
+ isn't on PATH in a pipx install. Migrations now run via the Python API."""
26
+ dbname = "migapi_" + uuid.uuid4().hex[:8]
27
+ admin = create_engine(pg_uri, isolation_level="AUTOCOMMIT")
28
+ with admin.connect() as conn:
29
+ conn.execute(text(f'CREATE DATABASE "{dbname}"'))
30
+ admin.dispose()
31
+
32
+ url = pg_uri.replace("/postgres?", f"/{dbname}?")
33
+ assert current_revision(url) is None # fresh database
34
+ upgrade_to_head(url)
35
+ assert current_revision(url) is not None # stamped after upgrade
36
+
37
+ eng = create_engine(url)
38
+ with eng.connect() as conn:
39
+ tables = set(conn.execute(text(
40
+ "SELECT table_name FROM information_schema.tables WHERE table_schema='public'"
41
+ )).scalars())
42
+ eng.dispose()
43
+ assert {"repositories", "symbols", "symbol_embeddings"} <= tables
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes