bgts-context-engine 0.2.2__tar.gz → 0.2.3__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 (125) hide show
  1. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/CHANGELOG.md +52 -1
  2. {bgts_context_engine-0.2.2/src/bgts_context_engine.egg-info → bgts_context_engine-0.2.3}/PKG-INFO +32 -3
  3. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/README.md +31 -2
  4. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/README.tr.md +31 -2
  5. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/__init__.py +1 -1
  6. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/ui/trace.py +4 -1
  7. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/bench/pr_ablation.py +27 -5
  8. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/cli.py +28 -4
  9. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/config.py +39 -3
  10. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/anchors.py +15 -5
  11. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/orchestrator.py +21 -5
  12. bgts_context_engine-0.2.3/src/bce/core/orchestrator/profile.py +125 -0
  13. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/embedder/__init__.py +4 -0
  14. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/embedder/encoder.py +164 -3
  15. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrator.py +64 -0
  16. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer3/orchestration.py +11 -2
  17. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3/src/bgts_context_engine.egg-info}/PKG-INFO +32 -3
  18. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bgts_context_engine.egg-info/SOURCES.txt +1 -0
  19. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/LICENSE +0 -0
  20. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/MANIFEST.in +0 -0
  21. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/pyproject.toml +0 -0
  22. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/setup.cfg +0 -0
  23. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/__init__.py +0 -0
  24. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/mcp/__init__.py +0 -0
  25. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/mcp/server.py +0 -0
  26. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/mcp/tools.py +0 -0
  27. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/__init__.py +0 -0
  28. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/app.py +0 -0
  29. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/deps.py +0 -0
  30. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/routes.py +0 -0
  31. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/schemas.py +0 -0
  32. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/static/assets/index-Cc4pUpSE.js +0 -0
  33. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/static/assets/index-DrLRaTMU.css +0 -0
  34. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/static/favicon.svg +0 -0
  35. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/static/index.html +0 -0
  36. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/ui/__init__.py +0 -0
  37. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/ui/queries.py +0 -0
  38. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/ui/router.py +0 -0
  39. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/api/rest/ui/schemas.py +0 -0
  40. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/bench/__init__.py +0 -0
  41. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/bench/runner.py +0 -0
  42. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/__init__.py +0 -0
  43. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/assembler/__init__.py +0 -0
  44. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/assembler/assembler.py +0 -0
  45. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/auth/__init__.py +0 -0
  46. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/auth/scope.py +0 -0
  47. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/coverage/__init__.py +0 -0
  48. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/coverage/confidence.py +0 -0
  49. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/i18n/__init__.py +0 -0
  50. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/i18n/catalogs/en.json +0 -0
  51. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/i18n/catalogs/tr.json +0 -0
  52. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/i18n/locale.py +0 -0
  53. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/i18n/translator.py +0 -0
  54. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/logging.py +0 -0
  55. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/__init__.py +0 -0
  56. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/bulk.py +0 -0
  57. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/expand.py +0 -0
  58. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/orchestrator/text.py +0 -0
  59. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/scoring/__init__.py +0 -0
  60. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/core/scoring/engine.py +0 -0
  61. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/domain/__init__.py +0 -0
  62. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/domain/enums.py +0 -0
  63. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/domain/models.py +0 -0
  64. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/__init__.py +0 -0
  65. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/churn.py +0 -0
  66. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/embedder/embedder.py +0 -0
  67. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/extractor/__init__.py +0 -0
  68. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/extractor/bridges.py +0 -0
  69. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/extractor/designnote.py +0 -0
  70. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/extractor/extractor.py +0 -0
  71. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/extractor/routes.py +0 -0
  72. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/gitsync/__init__.py +0 -0
  73. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/gitsync/bitbucket.py +0 -0
  74. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/gitsync/local.py +0 -0
  75. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/gitsync/remote.py +0 -0
  76. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/indexer.py +0 -0
  77. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/linker/__init__.py +0 -0
  78. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/linker/linker.py +0 -0
  79. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/__init__.py +0 -0
  80. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/_treesitter.py +0 -0
  81. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/base.py +0 -0
  82. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/__init__.py +0 -0
  83. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/csharp_provider.py +0 -0
  84. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/go_provider.py +0 -0
  85. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/java_provider.py +0 -0
  86. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/jsts_provider.py +0 -0
  87. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/languages/python_provider.py +0 -0
  88. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/registry.py +0 -0
  89. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/scip/__init__.py +0 -0
  90. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/scip/resolver.py +0 -0
  91. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/parser/symbol_id.py +0 -0
  92. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/upserter/__init__.py +0 -0
  93. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/indexing/upserter/upserter.py +0 -0
  94. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/jobs/__init__.py +0 -0
  95. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/jobs/store.py +0 -0
  96. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/jobs/worker.py +0 -0
  97. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/__init__.py +0 -0
  98. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/graph/__init__.py +0 -0
  99. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/graph/client.py +0 -0
  100. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/graph/repository.py +0 -0
  101. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/__init__.py +0 -0
  102. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/db.py +0 -0
  103. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0001_extensions_graph.sql +0 -0
  104. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0002_relational.sql +0 -0
  105. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0003_vector.sql +0 -0
  106. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0004_rls.sql +0 -0
  107. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0005_embeddings_dim.sql +0 -0
  108. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0006_fts.sql +0 -0
  109. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0007_jobs.sql +0 -0
  110. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0008_graph_label_indexes.sql +0 -0
  111. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0009_fts_identifier_parts.sql +0 -0
  112. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/migrations/0010_file_churn.sql +0 -0
  113. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/relational/queries.py +0 -0
  114. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/vector/__init__.py +0 -0
  115. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/storage/vector/store.py +0 -0
  116. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/__init__.py +0 -0
  117. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer1/__init__.py +0 -0
  118. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer1/primitives.py +0 -0
  119. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer2/__init__.py +0 -0
  120. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer2/search.py +0 -0
  121. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bce/tools/layer3/__init__.py +0 -0
  122. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bgts_context_engine.egg-info/dependency_links.txt +0 -0
  123. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bgts_context_engine.egg-info/entry_points.txt +0 -0
  124. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bgts_context_engine.egg-info/requires.txt +0 -0
  125. {bgts_context_engine-0.2.2 → bgts_context_engine-0.2.3}/src/bgts_context_engine.egg-info/top_level.txt +0 -0
@@ -7,6 +7,56 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.3] - 2026-09-21
11
+
12
+ ### Added
13
+
14
+ - `BCE_EMBEDDING_PROVIDER=openai`: an encoder that talks to any server speaking the OpenAI
15
+ `/v1/embeddings` protocol (vLLM, Text Embeddings Inference, Ollama, or OpenAI itself), using
16
+ only the standard library. This is how an open model such as `jinaai/jina-code-embeddings-1.5b`
17
+ runs on-prem. Query/document asymmetry is a text prefix — the jina-code family's `nl2code`
18
+ prompts are the default when the model name contains `jina-code`; anything else is unset
19
+ unless `BCE_EMBEDDING_QUERY_PREFIX` / `BCE_EMBEDDING_DOCUMENT_PREFIX` say otherwise.
20
+ `BCE_EMBEDDING_BASE_URL`, `BCE_EMBEDDING_API_KEY`, `BCE_EMBEDDING_EXTRA_BODY` (default
21
+ `{"truncate_prompt_tokens": -1}` so vLLM truncates instead of rejecting a long batch; set
22
+ `{}` for OpenAI's own API, which rejects unknown fields) and `BCE_EMBEDDING_TIMEOUT` (600 s)
23
+ configure the client. Vectors wider than `BCE_EMBEDDING_DIM` are truncated and re-normalised
24
+ (Matryoshka); a narrower reply is a configuration error. Transient 5xx / connection failures
25
+ retry three times; 4xx does not.
26
+ - Retrieval profiles (`bce.core.orchestrator.profile`): the three engine constants that encode
27
+ *where in the model's ranked list the right answers sit* — semantic-anchor rank scale, the
28
+ share of the answer reserved for the model's ranks, and a guard on its top ranks in
29
+ narrowing — grouped per embedding family. `BCE_RETRIEVAL_PROFILE=auto` (the default) picks
30
+ `voyage` for `voyage-*` and unknown models (the historical constants, unchanged) and `jina`
31
+ for `jina-*`. Name one to force it; `BCE_RETRIEVAL_SEMANTIC_GUARD_RANKS` /
32
+ `_RESERVE_SHARE` / `_RANK_SCALE` override a single knob for fitting runs. A profile change
33
+ never needs a re-index. The active profile is written into every `bce bench-prs` report.
34
+ - `bce migrate --reset-embeddings`: after the SQL migrations, `align_embedding_dim` re-types
35
+ `embeddings.embedding` to `BCE_EMBEDDING_DIM`. Stored vectors of another width are refused
36
+ unless the flag is set (they belong to another model and would be discarded); then the HNSW
37
+ index is rebuilt the same way migration `0005` did.
38
+
39
+ ### Changed
40
+
41
+ - Narrowing reads the active retrieval profile. The voyage path is unchanged (no guard,
42
+ share 0.5). The jina profile adds a guard of 10: the model's first ten ranks fill slots
43
+ 1–10 (only the container cap can skip one). jina-code finds more correct symbols than
44
+ voyage-code-4 but spreads them over ranks 1–15, and under the voyage constants the engine's
45
+ agreement with the model's *deeper* ranks satisfied the reserve share and pushed those mid
46
+ hits to positions 11–19 or out of the answer (PR replay, 30 PRs: recall@20 35.7 % → 38.2 %,
47
+ holdout 27.8 % → 34.9 %, retention of the model's own hits 83 % → 92 %). The list stays
48
+ K-monotonic.
49
+ - `bce bench-prs` titles the markdown with the encoder that actually produced the snapshots
50
+ (`jina-code-embeddings-1.5b vs … + BCE`), because the JSON key `voyage` is historical, not
51
+ the model.
52
+
53
+ ### Fixed
54
+
55
+ - `bce bench-prs --from-report` / `--rerun-bce` no longer treats a stored semantic pool
56
+ shorter than K as incomplete. A task whose neighbours are mostly tests legitimately has a
57
+ pool shorter than K — the live run had nothing more either — so the check is now "non-empty
58
+ and at least as long as the list scored from it".
59
+
10
60
  ## [0.2.2] - 2026-09-18
11
61
 
12
62
  ### Added
@@ -254,7 +304,8 @@ First public release.
254
304
  - Docker Compose deployment with PostgreSQL, Apache AGE and pgvector preconfigured.
255
305
  - `server.json` manifest describing the stdio server for the official MCP registry.
256
306
 
257
- [Unreleased]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.2...HEAD
307
+ [Unreleased]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.3...HEAD
308
+ [0.2.3]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.2...v0.2.3
258
309
  [0.2.2]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.1...v0.2.2
259
310
  [0.2.1]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.0...v0.2.1
260
311
  [0.2.0]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.1.0...v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bgts-context-engine
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Deterministic code-graph context engine for AI coding agents - PostgreSQL, Apache AGE and pgvector, over MCP and REST
5
5
  Author: Oğuz Öztürk, Enes İyidil
6
6
  Maintainer: BGTS
@@ -69,7 +69,7 @@ that actually answer it — ranked, budgeted, and reproducible.
69
69
  [![MCP](https://img.shields.io/badge/MCP-compatible-000000.svg)](docs/mcp.md)
70
70
  [![Stars](https://img.shields.io/github/stars/bgts-ai-org/bgts-context-engine?style=flat&logo=github)](https://github.com/bgts-ai-org/bgts-context-engine/stargazers)
71
71
 
72
- [Quick start](#quick-start) · [Use it from your agent](#use-it-from-your-agent) · [How it works](#how-it-works) · [Documentation](#documentation) · [Türkçe](README.tr.md)
72
+ [Quick start](#quick-start) · [Use it from your agent](#use-it-from-your-agent) · [How it works](#how-it-works) · [Supported models](#supported-models) · [Documentation](#documentation) · [Türkçe](README.tr.md)
73
73
 
74
74
  </div>
75
75
 
@@ -294,7 +294,36 @@ The full formula, every weight, and the confidence thresholds are in
294
294
  - **A UI that explains itself.** `/ui` ships in the wheel and replays a real retrieval call
295
295
  stage by stage: anchors lighting up, expansion spreading, candidates scored and cut.
296
296
  - **Runs offline.** The default embedding provider is deterministic arithmetic over token
297
- digests. No API key, no network, repeatable benchmarks.
297
+ digests. No API key, no network, repeatable benchmarks. `openai` talks to any
298
+ OpenAI-compatible `/v1/embeddings` server (vLLM, TEI, Ollama), so a model such as
299
+ [jina-code-embeddings-1.5b](https://huggingface.co/jinaai/jina-code-embeddings-1.5b)
300
+ can run inside the perimeter.
301
+
302
+ ## Supported models
303
+
304
+ Embeddings only find entry points when the task text names nothing the graph already
305
+ knows. They never rank the answer. Out of the box that seed is `hashing`: deterministic
306
+ arithmetic, no API key, no network. For a real code model, set `BCE_EMBEDDING_PROVIDER`
307
+ and `BCE_EMBEDDING_MODEL` to one of these:
308
+
309
+ | Model | Provider | Dimension |
310
+ | --- | --- | --- |
311
+ | [`voyage-code-3`](https://blog.voyageai.com/2024/12/04/voyage-code-3/) | Voyage AI (`voyage`) | 1024 |
312
+ | [`voyage-code-4`](https://blog.voyageai.com/2026/08/13/voyage-code-4/) | Voyage AI (`voyage`) | 1024 |
313
+ | [`jina-code-embeddings-1.5b`](https://huggingface.co/jinaai/jina-code-embeddings-1.5b) | OpenAI-compatible (`openai`) | 1536 |
314
+
315
+ Voyage is a hosted API — `pip install "bgts-context-engine[embed]"` and
316
+ `BCE_VOYAGE_API_KEY`. Jina is the on-prem path: any server that speaks `/v1/embeddings`
317
+ (vLLM, TEI, Ollama). Switching the model or the dimension is a re-index
318
+ (`bce migrate --reset-embeddings`). The knobs are in
319
+ [docs/deployment.md](docs/deployment.md).
320
+
321
+ **Coming next** — same `openai` socket, not yet a fitted retrieval profile:
322
+
323
+ - [`jina-code-embeddings-0.5b`](https://huggingface.co/jinaai/jina-code-embeddings-0.5b)
324
+ — the smaller sibling of 1.5b, for hosts that cannot hold 1.5B parameters.
325
+ - [`Nomic Embed Code`](https://huggingface.co/nomic-ai/nomic-embed-code) — an open 7B
326
+ code retriever.
298
327
 
299
328
  ## Where it fits
300
329
 
@@ -16,7 +16,7 @@ that actually answer it — ranked, budgeted, and reproducible.
16
16
  [![MCP](https://img.shields.io/badge/MCP-compatible-000000.svg)](docs/mcp.md)
17
17
  [![Stars](https://img.shields.io/github/stars/bgts-ai-org/bgts-context-engine?style=flat&logo=github)](https://github.com/bgts-ai-org/bgts-context-engine/stargazers)
18
18
 
19
- [Quick start](#quick-start) · [Use it from your agent](#use-it-from-your-agent) · [How it works](#how-it-works) · [Documentation](#documentation) · [Türkçe](README.tr.md)
19
+ [Quick start](#quick-start) · [Use it from your agent](#use-it-from-your-agent) · [How it works](#how-it-works) · [Supported models](#supported-models) · [Documentation](#documentation) · [Türkçe](README.tr.md)
20
20
 
21
21
  </div>
22
22
 
@@ -241,7 +241,36 @@ The full formula, every weight, and the confidence thresholds are in
241
241
  - **A UI that explains itself.** `/ui` ships in the wheel and replays a real retrieval call
242
242
  stage by stage: anchors lighting up, expansion spreading, candidates scored and cut.
243
243
  - **Runs offline.** The default embedding provider is deterministic arithmetic over token
244
- digests. No API key, no network, repeatable benchmarks.
244
+ digests. No API key, no network, repeatable benchmarks. `openai` talks to any
245
+ OpenAI-compatible `/v1/embeddings` server (vLLM, TEI, Ollama), so a model such as
246
+ [jina-code-embeddings-1.5b](https://huggingface.co/jinaai/jina-code-embeddings-1.5b)
247
+ can run inside the perimeter.
248
+
249
+ ## Supported models
250
+
251
+ Embeddings only find entry points when the task text names nothing the graph already
252
+ knows. They never rank the answer. Out of the box that seed is `hashing`: deterministic
253
+ arithmetic, no API key, no network. For a real code model, set `BCE_EMBEDDING_PROVIDER`
254
+ and `BCE_EMBEDDING_MODEL` to one of these:
255
+
256
+ | Model | Provider | Dimension |
257
+ | --- | --- | --- |
258
+ | [`voyage-code-3`](https://blog.voyageai.com/2024/12/04/voyage-code-3/) | Voyage AI (`voyage`) | 1024 |
259
+ | [`voyage-code-4`](https://blog.voyageai.com/2026/08/13/voyage-code-4/) | Voyage AI (`voyage`) | 1024 |
260
+ | [`jina-code-embeddings-1.5b`](https://huggingface.co/jinaai/jina-code-embeddings-1.5b) | OpenAI-compatible (`openai`) | 1536 |
261
+
262
+ Voyage is a hosted API — `pip install "bgts-context-engine[embed]"` and
263
+ `BCE_VOYAGE_API_KEY`. Jina is the on-prem path: any server that speaks `/v1/embeddings`
264
+ (vLLM, TEI, Ollama). Switching the model or the dimension is a re-index
265
+ (`bce migrate --reset-embeddings`). The knobs are in
266
+ [docs/deployment.md](docs/deployment.md).
267
+
268
+ **Coming next** — same `openai` socket, not yet a fitted retrieval profile:
269
+
270
+ - [`jina-code-embeddings-0.5b`](https://huggingface.co/jinaai/jina-code-embeddings-0.5b)
271
+ — the smaller sibling of 1.5b, for hosts that cannot hold 1.5B parameters.
272
+ - [`Nomic Embed Code`](https://huggingface.co/nomic-ai/nomic-embed-code) — an open 7B
273
+ code retriever.
245
274
 
246
275
  ## Where it fits
247
276
 
@@ -14,7 +14,7 @@ veren sekiz sembolü sıralanmış, bütçelenmiş ve yeniden üretilebilir şek
14
14
  [![MCP](https://img.shields.io/badge/MCP-uyumlu-000000.svg)](docs/mcp.md)
15
15
  [![Yıldızlar](https://img.shields.io/github/stars/bgts-ai-org/bgts-context-engine?style=flat&logo=github)](https://github.com/bgts-ai-org/bgts-context-engine/stargazers)
16
16
 
17
- [Hızlı başlangıç](#hızlı-başlangıç) · [Ajanınızdan kullanma](#ajanınızdan-kullanma) · [Nasıl çalışır](#nasıl-çalışır) · [Dokümantasyon](#dokümantasyon) · [English](README.md)
17
+ [Hızlı başlangıç](#hızlı-başlangıç) · [Ajanınızdan kullanma](#ajanınızdan-kullanma) · [Nasıl çalışır](#nasıl-çalışır) · [Desteklenen modeller](#desteklenen-modeller) · [Dokümantasyon](#dokümantasyon) · [English](README.md)
18
18
 
19
19
  </div>
20
20
 
@@ -245,7 +245,36 @@ Formülün tamamı, her ağırlık ve güven eşikleri
245
245
  aşama aşama oynatır: çapaların yanması, genişlemenin yayılması, adayların skorlanıp
246
246
  kesilmesi.
247
247
  - **Çevrimdışı çalışır.** Varsayılan embedding sağlayıcısı, token özetleri üzerinde
248
- deterministik aritmetiktir. API anahtarı yok, ağ yok, tekrarlanabilir ölçümler.
248
+ deterministik aritmetiktir. API anahtarı yok, ağ yok, tekrarlanabilir ölçümler. `openai`
249
+ sağlayıcısı OpenAI uyumlu herhangi bir `/v1/embeddings` sunucusuna konuşur (vLLM, TEI,
250
+ Ollama); [jina-code-embeddings-1.5b](https://huggingface.co/jinaai/jina-code-embeddings-1.5b)
251
+ gibi bir model çevre içinde çalışır.
252
+
253
+ ## Desteklenen modeller
254
+
255
+ Embedding yalnızca grafın tanımadığı bir görev metninde giriş noktası bulur. Cevabı asla
256
+ sıralamaz. Varsayılan `hashing`'dir: deterministik aritmetik, API anahtarı yok, ağ yok.
257
+ Gerçek bir kod modeli için `BCE_EMBEDDING_PROVIDER` ve `BCE_EMBEDDING_MODEL`'i şunlardan
258
+ birine ayarlayın:
259
+
260
+ | Model | Sağlayıcı | Boyut |
261
+ | --- | --- | --- |
262
+ | [`voyage-code-3`](https://blog.voyageai.com/2024/12/04/voyage-code-3/) | Voyage AI (`voyage`) | 1024 |
263
+ | [`voyage-code-4`](https://blog.voyageai.com/2026/08/13/voyage-code-4/) | Voyage AI (`voyage`) | 1024 |
264
+ | [`jina-code-embeddings-1.5b`](https://huggingface.co/jinaai/jina-code-embeddings-1.5b) | OpenAI uyumlu (`openai`) | 1536 |
265
+
266
+ Voyage barındırılan bir API'dir — `pip install "bgts-context-engine[embed]"` ve
267
+ `BCE_VOYAGE_API_KEY`. Jina çevre-içi yoldur: `/v1/embeddings` konuşan herhangi bir
268
+ sunucu (vLLM, TEI, Ollama). Model veya boyutu değiştirmek yeniden indekslemedir
269
+ (`bce migrate --reset-embeddings`). Ayarlar
270
+ [docs/deployment.md](docs/deployment.md) içindedir.
271
+
272
+ **Sırada** — aynı `openai` soketi, henüz ayrı bir getirme profili yok:
273
+
274
+ - [`jina-code-embeddings-0.5b`](https://huggingface.co/jinaai/jina-code-embeddings-0.5b)
275
+ — 1.5b'nin küçük kardeşi; 1.5B parametreyi taşıyamayan makineler için.
276
+ - [`Nomic Embed Code`](https://huggingface.co/nomic-ai/nomic-embed-code) — açık kaynaklı
277
+ 7B kod getiricisi.
249
278
 
250
279
  ## Nereye oturur
251
280
 
@@ -5,4 +5,4 @@ specification for details. This package is the provider side of the deterministi
5
5
  contains no LLM and produces reproducible context packages.
6
6
  """
7
7
 
8
- __version__ = "0.2.2"
8
+ __version__ = "0.2.3"
@@ -21,6 +21,7 @@ from bce.core.coverage import compute_coverage
21
21
  from bce.core.orchestrator.anchors import find_anchors
22
22
  from bce.core.orchestrator.expand import expand_from_anchors, to_candidates
23
23
  from bce.core.orchestrator.orchestrator import RetrievalResult, narrow
24
+ from bce.core.orchestrator.profile import active_profile
24
25
  from bce.core.orchestrator.text import content_tokens
25
26
  from bce.core.scoring.engine import ScoreWeights, score_candidates
26
27
  from bce.storage.graph.repository import GraphRepository
@@ -69,6 +70,7 @@ def trace_context_for_task(
69
70
 
70
71
  # Stage 1: multi-source anchors (spec section 6.2), with evidence strength per anchor.
71
72
  t = time.perf_counter()
73
+ profile = active_profile()
72
74
  anchors = find_anchors(
73
75
  repository,
74
76
  task_text=task_text,
@@ -78,6 +80,7 @@ def trace_context_for_task(
78
80
  component_repo_ids=None,
79
81
  repo_ids=repo_ids,
80
82
  semantic_candidates=semantic_candidates,
83
+ profile=profile,
81
84
  )
82
85
  stages.append(
83
86
  {
@@ -139,7 +142,7 @@ def trace_context_for_task(
139
142
 
140
143
  # Stage 4: diversity-aware narrowing to top-N (the "1000 -> 8" step).
141
144
  t = time.perf_counter()
142
- narrowed = narrow(ranked, max_candidates)
145
+ narrowed = narrow(ranked, max_candidates, profile=profile)
143
146
  stages.append(
144
147
  {
145
148
  "stage": "narrow",
@@ -666,6 +666,9 @@ class AblationReport:
666
666
  cases: list[CaseOutcome]
667
667
  #: Extra K columns derived from the K-long lists (see :func:`score_runs`).
668
668
  ks: list[int] = field(default_factory=list)
669
+ #: The retrieval profile BCE ran with (name + the model-specific constants), so two runs of
670
+ #: the same snapshots can be told apart by their engine tuning.
671
+ profile: dict[str, Any] = field(default_factory=dict)
669
672
 
670
673
  def _rows(
671
674
  self, variant: str, *, scored_only: bool, split: str | None, level: str = "symbol"
@@ -848,6 +851,7 @@ class AblationReport:
848
851
  "k": self.k,
849
852
  "ks": list(self.ks),
850
853
  "encoder_model": self.encoder_model,
854
+ "profile": dict(self.profile),
851
855
  "variants": list(self.variants),
852
856
  "summary": {v: self.aggregate(v) for v in self.variants},
853
857
  "summary_by_split": {
@@ -1124,10 +1128,14 @@ def run_ablation(
1124
1128
  for system in SYSTEMS
1125
1129
  if replay is not None and (case.spec.number, variant, system) in replay
1126
1130
  }
1131
+ # The stored pool is complete when it is at least as long as the list that was
1132
+ # scored from it: a task whose neighbours are mostly tests legitimately has a
1133
+ # pool shorter than K (the live run had nothing more either).
1127
1134
  baselines_ok = (
1128
1135
  "voyage" in replayed
1129
1136
  and "voyage_raw" in replayed
1130
- and len(replayed["voyage"].semantic_pool) >= k
1137
+ and bool(replayed["voyage"].semantic_pool)
1138
+ and len(replayed["voyage"].semantic_pool) >= len(replayed["voyage"].returned)
1131
1139
  )
1132
1140
  if len(replayed) == len(SYSTEMS) and not rerun_bce:
1133
1141
  runs = replayed
@@ -1157,8 +1165,15 @@ def run_ablation(
1157
1165
  outcomes.append(outcome)
1158
1166
  if progress is not None:
1159
1167
  progress(outcome)
1168
+ from bce.core.orchestrator.profile import active_profile
1169
+
1160
1170
  return AblationReport(
1161
- k=k, encoder_model=encoder_model, variants=list(variants), cases=outcomes, ks=list(ks)
1171
+ k=k,
1172
+ encoder_model=encoder_model,
1173
+ variants=list(variants),
1174
+ cases=outcomes,
1175
+ ks=list(ks),
1176
+ profile=asdict(active_profile()),
1162
1177
  )
1163
1178
 
1164
1179
 
@@ -1355,9 +1370,15 @@ def _render_text_buckets(lines: list[str], buckets: dict[str, Any]) -> None:
1355
1370
 
1356
1371
  def render_markdown(report: AblationReport) -> str:
1357
1372
  lines: list[str] = []
1358
- lines.append("# Voyage vs Voyage + BCE - PR replay benchmark")
1373
+ # The pure-embedding baseline is keyed ``voyage`` in the JSON for historical reasons; the
1374
+ # actual model is whatever encoded the snapshots, so the title says that instead.
1375
+ model_name = report.encoder_model.rpartition("-")[0] or report.encoder_model
1376
+ lines.append(f"# {model_name} vs {model_name} + BCE - PR replay benchmark")
1359
1377
  lines.append("")
1360
1378
  lines.append(f"- Encoder: `{report.encoder_model}` (same embedding rows for both systems)")
1379
+ if report.profile:
1380
+ knobs = ", ".join(f"{k}={v}" for k, v in report.profile.items() if k != "name")
1381
+ lines.append(f"- Retrieval profile: `{report.profile.get('name', '?')}` ({knobs})")
1361
1382
  lines.append(f"- K (results per system): {report.k}")
1362
1383
  scored = sorted({c.number for c in report.cases if c.scored})
1363
1384
  unscored = sorted({c.number for c in report.cases if not c.scored})
@@ -1370,8 +1391,9 @@ def render_markdown(report: AblationReport) -> str:
1370
1391
  lines.append("")
1371
1392
  lines.append(
1372
1393
  "Ground truth = symbols of the pre-PR snapshot that the PR's diff touched (tests and "
1373
- "files created by the PR excluded). `voyage` = nearest non-test symbol embeddings; "
1374
- "`voyage_raw` = unfiltered nearest neighbours; `bce` = full engine with the same "
1394
+ f"files created by the PR excluded). `voyage` = pure `{model_name}` search, nearest "
1395
+ f"non-test symbol embeddings (the key is historical, not the model); `voyage_raw` = "
1396
+ "unfiltered nearest neighbours; `bce` = full engine with the same "
1375
1397
  "neighbours as semantic anchors. Retention = share of `voyage`'s correct hits the system "
1376
1398
  "still returns. Engine constants were fitted on `tune` only, so `holdout` is the honest "
1377
1399
  "number."
@@ -69,15 +69,39 @@ def _root(
69
69
 
70
70
 
71
71
  @app.command()
72
- def migrate() -> None:
73
- """Apply pending database migrations."""
74
- from bce.storage.relational.migrator import run_migrations
72
+ def migrate(
73
+ reset_embeddings: bool = typer.Option(
74
+ False,
75
+ "--reset-embeddings",
76
+ help=(
77
+ "Allow re-typing embeddings.embedding to BCE_EMBEDDING_DIM even when it holds vectors "
78
+ "of another width (they are discarded; reindex afterwards)"
79
+ ),
80
+ ),
81
+ ) -> None:
82
+ """Apply pending database migrations and fit the vector column to BCE_EMBEDDING_DIM."""
83
+ from bce.config import get_settings
84
+ from bce.storage.relational.db import connection
85
+ from bce.storage.relational.migrator import (
86
+ EmbeddingDimMismatch,
87
+ align_embedding_dim,
88
+ run_migrations,
89
+ )
75
90
 
76
- applied = run_migrations()
91
+ dim = get_settings().embedding_dim
92
+ with connection() as conn:
93
+ applied = run_migrations(conn)
94
+ try:
95
+ resized = align_embedding_dim(conn, dim, reset=reset_embeddings)
96
+ except EmbeddingDimMismatch as exc:
97
+ typer.echo(f"Configuration error: {exc}", err=True)
98
+ raise typer.Exit(code=1) from exc
77
99
  if applied:
78
100
  typer.echo("Applied migrations: " + ", ".join(applied))
79
101
  else:
80
102
  typer.echo("No pending migrations.")
103
+ if resized:
104
+ typer.echo(f"Re-typed embeddings.embedding to vector({dim}).")
81
105
 
82
106
 
83
107
  @app.command()
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import os
15
15
  from functools import lru_cache
16
+ from typing import Any
16
17
 
17
18
  from pydantic import Field
18
19
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -61,19 +62,54 @@ class Settings(BaseSettings):
61
62
  cors_origins: tuple[str, ...] = ("http://localhost:5173", "http://127.0.0.1:5173")
62
63
 
63
64
  # --- Embedding (Phase 2; pinned for determinism, P2) ---
64
- # Provider selects the encoder: "hashing" (default, dependency-free, deterministic fallback) or
65
- # "voyage" (Voyage AI code embeddings). Model + dim are pinned; a change is a reindex boundary.
65
+ # Provider selects the encoder: "hashing" (default, dependency-free, deterministic fallback),
66
+ # "voyage" (Voyage AI code embeddings) or "openai" (any OpenAI-compatible ``/v1/embeddings``
67
+ # server: vLLM, TEI, Ollama, OpenAI itself - the way to run an open model such as
68
+ # jinaai/jina-code-embeddings-1.5b). Model + dim are pinned; a change is a reindex boundary.
66
69
  embedding_provider: str = Field(
67
70
  default="hashing",
68
- description="Embedding backend: 'hashing' (local fallback) or 'voyage' (Voyage AI).",
71
+ description=(
72
+ "Embedding backend: 'hashing' (local fallback), 'voyage' (Voyage AI) or 'openai' "
73
+ "(OpenAI-compatible HTTP endpoint)."
74
+ ),
69
75
  )
70
76
  embedding_model: str = Field(
71
77
  default="voyage-code-3",
72
78
  description="Pinned embedding model identifier. Versioned for reproducibility (P2).",
73
79
  )
80
+ # Width of the pgvector column. ``bce migrate`` re-types ``embeddings.embedding`` to this width;
81
+ # an encoder returning wider vectors is truncated to it and re-normalised (Matryoshka models).
74
82
  embedding_dim: int = 1024
75
83
  # Voyage AI API key (used only when embedding_provider == 'voyage').
76
84
  voyage_api_key: str = ""
85
+ # --- provider "openai" only ---
86
+ # Base URL of the OpenAI-compatible server; ``/embeddings`` is appended.
87
+ embedding_base_url: str = "http://127.0.0.1:8001/v1"
88
+ # Bearer token; leave empty for a local server that does not check one.
89
+ embedding_api_key: str = ""
90
+ # Instruction prefixes prepended to queries / documents (instruction-tuned models). ``None``
91
+ # picks a built-in default from the model name (jina-code-* -> its nl2code prompts); set a
92
+ # string - even an empty one - to override.
93
+ embedding_query_prefix: str | None = None
94
+ embedding_document_prefix: str | None = None
95
+ # Extra JSON fields merged into every request body. The default asks vLLM to truncate inputs
96
+ # longer than its ``--max-model-len`` instead of rejecting the batch; other servers ignore the
97
+ # field, OpenAI's own API rejects unknown fields, so set it to ``{}`` there.
98
+ embedding_extra_body: dict[str, Any] = Field(
99
+ default_factory=lambda: {"truncate_prompt_tokens": -1}
100
+ )
101
+ # Seconds to wait for one embedding request (a local model on a laptop can need minutes).
102
+ embedding_timeout: float = 600.0
103
+
104
+ # --- Retrieval profile (engine constants that depend on the embedding model) ---
105
+ # "auto" (default) picks the profile fitted for ``embedding_model`` - "voyage" for voyage-*,
106
+ # "jina" for jina-*, voyage's constants for anything else; or name one explicitly. The three
107
+ # optional knobs override single values of the chosen profile (fitting runs; leave unset).
108
+ # See ``bce.core.orchestrator.profile``.
109
+ retrieval_profile: str = "auto"
110
+ retrieval_semantic_guard_ranks: int | None = None
111
+ retrieval_semantic_reserve_share: float | None = None
112
+ retrieval_semantic_rank_scale: float | None = None
77
113
 
78
114
  # --- Logging (JSON to stdout; optional rotating file sink) ---
79
115
  log_level: str = "INFO"
@@ -37,6 +37,7 @@ import re
37
37
  from dataclasses import dataclass, field
38
38
 
39
39
  from bce.core.orchestrator import bulk
40
+ from bce.core.orchestrator.profile import RetrievalProfile
40
41
  from bce.core.orchestrator.text import (
41
42
  explicit_references,
42
43
  is_test_symbol,
@@ -86,6 +87,8 @@ _SATURATED_TERM_WEIGHT = 0.35
86
87
  #: ``rank / len(list)`` form made rank 29 of a 30-list as strong as rank 10 of an 11-list).
87
88
  #: Measured on the PR-replay tune split: 8 / 10 / 15 all lost semantic retention (80 %) and
88
89
  #: recall@20 against 30, which keeps every model rank in the pool; steeper decay is not paid for.
90
+ #: This is the voyage-code-4 value; another embedding model may carry its own in its
91
+ #: :class:`~bce.core.orchestrator.profile.RetrievalProfile` (``semantic_rank_scale``).
89
92
  SEMANTIC_RANK_SCALE = 30
90
93
 
91
94
 
@@ -144,11 +147,13 @@ def find_anchors(
144
147
  lexical_terms: list[str] | None = None,
145
148
  semantic_candidates: list[str] | None = None,
146
149
  include_tests: bool = False,
150
+ profile: RetrievalProfile | None = None,
147
151
  ) -> AnchorResult:
148
152
  """Combine the four anchor sources deterministically. Callers supply pre-fetched metadata.
149
153
 
150
154
  ``semantic_candidates`` must be in the model's rank order (best first); rank decides the
151
155
  semantic evidence strength. ``include_tests`` admits test symbols from lexical/semantic.
156
+ ``profile`` supplies the model-specific rank scale (default: the voyage constants).
152
157
  """
153
158
  result = AnchorResult()
154
159
  for rid in sorted(set(component_repo_ids or [])):
@@ -181,7 +186,10 @@ def find_anchors(
181
186
  result.add(sid, "lexical", strength)
182
187
 
183
188
  # Source 4b: semantic candidates (lowest priority; caller supplies the model's ranked list).
184
- for sid, rank, strength in _semantic_anchors(repository, semantic_candidates, include_tests):
189
+ scale = profile.semantic_rank_scale if profile is not None else SEMANTIC_RANK_SCALE
190
+ for sid, rank, strength in _semantic_anchors(
191
+ repository, semantic_candidates, include_tests, scale=scale
192
+ ):
185
193
  result.add(sid, "semantic", strength)
186
194
  result.semantic_rank[sid] = rank
187
195
 
@@ -246,11 +254,13 @@ def _semantic_anchors(
246
254
  repository: GraphRepository,
247
255
  semantic_candidates: list[str] | None,
248
256
  include_tests: bool,
257
+ *,
258
+ scale: float = SEMANTIC_RANK_SCALE,
249
259
  ) -> list[tuple[str, int, float]]:
250
260
  """``(symbol_id, rank, strength)`` for the model's ranked list, strongest first.
251
261
 
252
262
  Strength decays hyperbolically with the absolute rank - ``cap / (1 + rank / scale)``
253
- (:data:`SEMANTIC_RANK_SCALE`) - so the best hit is worth as much as a moderately ambiguous
263
+ (:data:`SEMANTIC_RANK_SCALE` unless the profile says otherwise) - so the best hit is worth as much as a moderately ambiguous
254
264
  explicit name, mid ranks still expand, and the tail of a wide pool is kept as candidates only.
255
265
  Only the rank enters the engine: a similarity number would let the model reorder candidates,
256
266
  which is the one thing the deterministic core does not delegate.
@@ -265,13 +275,13 @@ def _semantic_anchors(
265
275
  info = meta.get(sid) or {}
266
276
  if is_test_symbol(sid, info.get("name"), info.get("file_id")):
267
277
  continue
268
- out.append((sid, rank, semantic_strength(rank)))
278
+ out.append((sid, rank, semantic_strength(rank, scale=scale)))
269
279
  return out
270
280
 
271
281
 
272
- def semantic_strength(rank: int) -> float:
282
+ def semantic_strength(rank: int, *, scale: float = SEMANTIC_RANK_SCALE) -> float:
273
283
  """Anchor strength for the model's 0-based ``rank`` (see :data:`SEMANTIC_RANK_SCALE`)."""
274
- return round(SOURCE_STRENGTH["semantic"] / (1.0 + max(rank, 0) / SEMANTIC_RANK_SCALE), 6)
284
+ return round(SOURCE_STRENGTH["semantic"] / (1.0 + max(rank, 0) / float(scale)), 6)
275
285
 
276
286
 
277
287
  def _lexical_anchors(
@@ -25,6 +25,7 @@ from dataclasses import dataclass, field
25
25
 
26
26
  from bce.core.orchestrator.anchors import AnchorResult
27
27
  from bce.core.orchestrator.expand import expand_from_anchors, to_candidates
28
+ from bce.core.orchestrator.profile import RetrievalProfile
28
29
  from bce.core.orchestrator.text import query_terms
29
30
  from bce.core.scoring.engine import (
30
31
  CONTAINER_KINDS,
@@ -59,6 +60,8 @@ PER_CONTAINER_SHARE = 0.3
59
60
  #: that split is where the model is weak (recall 10 %); on the holdout split, where the model is
60
61
  #: stronger (18 %), 0.3 let three of the model's own hits slip from the top 5 / out of the list.
61
62
  #: 0.5 is the a-priori choice: half the list is the model's, whatever the engine thinks.
63
+ #: This is the voyage-code-4 value; another embedding model may carry its own share and a
64
+ #: guard on its top ranks in its :class:`~bce.core.orchestrator.profile.RetrievalProfile`.
62
65
  SEMANTIC_RESERVE_SHARE = 0.5
63
66
  #: Diversity rule 3: at most ceil(n * share) container symbols among the first n (a class is a
64
67
  #: location, not a change site; its members carry the change).
@@ -95,9 +98,12 @@ class RetrievalOrchestrator:
95
98
  self,
96
99
  repository: GraphRepository,
97
100
  weights: ScoreWeights | None = None,
101
+ profile: RetrievalProfile | None = None,
98
102
  ) -> None:
99
103
  self.repository = repository
100
104
  self.weights = weights or ScoreWeights()
105
+ #: Model-specific narrowing constants; ``None`` = the voyage defaults.
106
+ self.profile = profile
101
107
 
102
108
  def retrieve(
103
109
  self,
@@ -141,7 +147,7 @@ class RetrievalOrchestrator:
141
147
  timings["score_ms"] = _elapsed_ms(stage)
142
148
 
143
149
  stage = time.perf_counter()
144
- narrowed = narrow(ranked, max_candidates)
150
+ narrowed = narrow(ranked, max_candidates, profile=self.profile)
145
151
  timings["narrow_ms"] = _elapsed_ms(stage)
146
152
 
147
153
  signals = {c.symbol_id: c.task_signal for c in ranked if c.task_signal > 0.0}
@@ -168,7 +174,9 @@ def _container_of(cand: Candidate) -> str | None:
168
174
  return "::".join(segments[:-1])
169
175
 
170
176
 
171
- def narrow(ranked: list[Candidate], max_candidates: int) -> list[Candidate]:
177
+ def narrow(
178
+ ranked: list[Candidate], max_candidates: int, *, profile: RetrievalProfile | None = None
179
+ ) -> list[Candidate]:
172
180
  """Deterministic, diversity-aware top-N over an already ranked (score desc, id asc) list.
173
181
 
174
182
  The list is built one slot at a time from two streams - the embedding model's ranks (rank
@@ -176,9 +184,15 @@ def narrow(ranked: list[Candidate], max_candidates: int) -> list[Candidate]:
176
184
  ``narrow(pool, n) == narrow(pool, n + 1)[:n]``: asking for a longer list never changes what
177
185
  the shorter one was (K-monotonic), and a K=20 run can be cut to K=10 exactly.
178
186
 
179
- Slot ``i`` (1-based) goes to:
187
+ ``profile`` supplies the two model-specific constants (default: the voyage values,
188
+ :data:`SEMANTIC_RESERVE_SHARE` and no guard). Slot ``i`` (1-based) goes to:
180
189
 
181
- 1. the model's next rank while fewer than ``i * SEMANTIC_RESERVE_SHARE`` slots came from the
190
+ 0. the model's next rank while ``i <= profile.semantic_guard_ranks`` (the guard: the model's
191
+ first ``n`` ranks *are* slots 1..n, whatever the engine scored - only the container cap
192
+ can skip one). A model whose correct hits spread over ranks 1-15 (jina) needs this;
193
+ without it the engine's agreement with the model's *deeper* ranks satisfies the share
194
+ below and its own picks push the model's ranks 7-15 to positions 11-19 or out;
195
+ 1. the model's next rank while fewer than ``i * semantic_reserve_share`` slots came from the
182
196
  model stream (so position 1 is always the model's #1, and the model's top ranks can never
183
197
  be ranked out of the answer);
184
198
  2. else the engine's head (score order). An engine pick that *is* one of the model's ranks
@@ -206,6 +220,8 @@ def narrow(ranked: list[Candidate], max_candidates: int) -> list[Candidate]:
206
220
  """
207
221
  if max_candidates <= 0 or not ranked:
208
222
  return []
223
+ reserve_share = SEMANTIC_RESERVE_SHARE if profile is None else profile.semantic_reserve_share
224
+ guard_ranks = 0 if profile is None else max(0, int(profile.semantic_guard_ranks))
209
225
 
210
226
  model_stream = sorted(
211
227
  (c for c in ranked if c.semantic_rank is not None),
@@ -261,7 +277,7 @@ def narrow(ranked: list[Candidate], max_candidates: int) -> list[Candidate]:
261
277
  if model_head is None and engine_head is None:
262
278
  break
263
279
  if model_head is not None and (
264
- engine_head is None or model_taken < slot * SEMANTIC_RESERVE_SHARE
280
+ engine_head is None or slot <= guard_ranks or model_taken < slot * reserve_share
265
281
  ):
266
282
  take(model_head, from_model=True)
267
283
  elif engine_head is not None: