bgts-context-engine 0.2.1__tar.gz → 0.2.2__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.
- bgts_context_engine-0.2.2/CHANGELOG.md +261 -0
- {bgts_context_engine-0.2.1/src/bgts_context_engine.egg-info → bgts_context_engine-0.2.2}/PKG-INFO +1 -1
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/__init__.py +1 -1
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/trace.py +29 -112
- bgts_context_engine-0.2.2/src/bce/bench/pr_ablation.py +1413 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/bench/runner.py +1 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/cli.py +242 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/assembler/assembler.py +37 -1
- bgts_context_engine-0.2.2/src/bce/core/coverage/confidence.py +259 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/catalogs/en.json +3 -3
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/catalogs/tr.json +3 -3
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/orchestrator/__init__.py +4 -2
- bgts_context_engine-0.2.2/src/bce/core/orchestrator/anchors.py +336 -0
- bgts_context_engine-0.2.2/src/bce/core/orchestrator/bulk.py +117 -0
- bgts_context_engine-0.2.2/src/bce/core/orchestrator/expand.py +383 -0
- bgts_context_engine-0.2.2/src/bce/core/orchestrator/orchestrator.py +278 -0
- bgts_context_engine-0.2.2/src/bce/core/orchestrator/text.py +490 -0
- bgts_context_engine-0.2.2/src/bce/core/scoring/engine.py +327 -0
- bgts_context_engine-0.2.2/src/bce/indexing/churn.py +122 -0
- bgts_context_engine-0.2.2/src/bce/indexing/embedder/embedder.py +130 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/embedder/encoder.py +38 -7
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/indexer.py +37 -2
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/repository.py +401 -0
- bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0008_graph_label_indexes.sql +38 -0
- bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0009_fts_identifier_parts.sql +55 -0
- bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0010_file_churn.sql +21 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer3/orchestration.py +104 -62
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2/src/bgts_context_engine.egg-info}/PKG-INFO +1 -1
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/SOURCES.txt +7 -0
- bgts_context_engine-0.2.1/CHANGELOG.md +0 -92
- bgts_context_engine-0.2.1/src/bce/core/coverage/confidence.py +0 -142
- bgts_context_engine-0.2.1/src/bce/core/orchestrator/anchors.py +0 -119
- bgts_context_engine-0.2.1/src/bce/core/orchestrator/expand.py +0 -167
- bgts_context_engine-0.2.1/src/bce/core/orchestrator/orchestrator.py +0 -77
- bgts_context_engine-0.2.1/src/bce/core/scoring/engine.py +0 -106
- bgts_context_engine-0.2.1/src/bce/indexing/embedder/embedder.py +0 -75
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/LICENSE +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/MANIFEST.in +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/README.md +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/README.tr.md +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/pyproject.toml +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/setup.cfg +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/server.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/tools.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/app.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/deps.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/routes.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/schemas.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/assets/index-Cc4pUpSE.js +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/assets/index-DrLRaTMU.css +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/favicon.svg +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/index.html +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/queries.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/router.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/schemas.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/bench/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/config.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/assembler/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/auth/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/auth/scope.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/coverage/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/locale.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/translator.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/logging.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/scoring/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/enums.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/models.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/embedder/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/bridges.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/designnote.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/extractor.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/routes.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/bitbucket.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/local.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/remote.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/linker/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/linker/linker.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/_treesitter.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/base.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/csharp_provider.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/go_provider.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/java_provider.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/jsts_provider.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/python_provider.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/registry.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/scip/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/scip/resolver.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/symbol_id.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/upserter/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/upserter/upserter.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/store.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/worker.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/client.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/db.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0001_extensions_graph.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0002_relational.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0003_vector.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0004_rls.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0005_embeddings_dim.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0006_fts.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0007_jobs.sql +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrator.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/queries.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/vector/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/vector/store.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer1/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer1/primitives.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer2/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer2/search.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer3/__init__.py +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/dependency_links.txt +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/entry_points.txt +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/requires.txt +0 -0
- {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.2] - 2026-09-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `bce bench-prs prepare` / `run`: a PR replay ablation that measures what the engine adds on
|
|
15
|
+
top of the embedding model. Each merged PR is replayed against a database indexing the repo
|
|
16
|
+
at the PR's base commit; the title (+ description) is the task and the symbols the diff
|
|
17
|
+
touched in that snapshot are the ground truth. `voyage` (nearest non-test symbol embeddings),
|
|
18
|
+
`voyage_raw` and `bce` (the same neighbours as semantic anchors + explicit/lexical anchors,
|
|
19
|
+
expansion, scoring, narrowing) are scored with Recall/Precision/F1/MRR/Hit@K, a lenient
|
|
20
|
+
recall that credits enclosing symbols, file-level recall/precision, latency and a breakdown
|
|
21
|
+
of returned symbol kinds. `--from-report` re-scores stored result lists without querying.
|
|
22
|
+
`--tune` splits the PRs into a tune and a holdout half and reports each separately, so the
|
|
23
|
+
constants can be fitted on one half and quoted from the other; `--only` narrows a run to a
|
|
24
|
+
few PRs. Each `bce` case also reports `semantic_retention` (the share of the embedding
|
|
25
|
+
model's correct hits the engine still returns) and per-stage timings.
|
|
26
|
+
- `suggest_change_sites` returns a `timings` block (`semantic_ms`, `anchors_ms`, `expand_ms`,
|
|
27
|
+
`candidates_ms`, `score_ms`, `narrow_ms`, `enrich_ms`, `total_ms`); `trace=True` also returns
|
|
28
|
+
the ranked pool before narrowing (`ranked_ids`), for diagnostics.
|
|
29
|
+
- `bce bench-prs run --k 5,10,20` runs once at the largest K and reports every K as an extra
|
|
30
|
+
column (`metrics_at`) - exact because both lists are prefix chains (see narrowing below).
|
|
31
|
+
Each `bce` case carries a `truth_trace`: for every ground-truth symbol, the channel that
|
|
32
|
+
brought it in (`semantic` / `anchor` / `expansion` / none), the stage where it was lost
|
|
33
|
+
(`not_reached`, `narrowed_out`, `returned`), its rank in the model's list, in the scored pool
|
|
34
|
+
and in the answer. The report aggregates these per split and per K, together with a
|
|
35
|
+
task-text axis (`task_stats`: characters, content tokens, code identifiers in the PR text) so
|
|
36
|
+
a "no identifiers in the description" PR can be told from an engine miss. PRs whose diff
|
|
37
|
+
touches no symbol in the snapshot are scored at file level (`file_recall` / `file_precision`)
|
|
38
|
+
instead of being dropped. `--rerun-bce` replays the stored embedding neighbours and re-runs
|
|
39
|
+
only the engine, which turns a constants sweep from an API-bound run into ~100 s per split.
|
|
40
|
+
- `bce churn --repo --name [--commit] [--db-name]` (re)computes per-file git churn for an
|
|
41
|
+
indexed repository; the indexer records it on every run (migration `0010_file_churn`).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Retrieval scoring is now `scoring-v3`. Anchors carry an evidence *strength* in `[0, 1]`
|
|
46
|
+
(noisy-OR over the sources that nominated them) and the flat `+5.0` anchor floor became
|
|
47
|
+
`3.5 · strength`. On natural-language tasks the floor had split the pool into two disjoint
|
|
48
|
+
bands — every anchor above 7.3, every graph neighbour below 4.9 — so the top-N was the
|
|
49
|
+
highest-degree anchors regardless of evidence, and the functions that actually needed the
|
|
50
|
+
change ranked #25–#478. Proximity is `1 / (1 + distance)` (an anchor and its first hop no
|
|
51
|
+
longer score the same), centrality is log-scaled with weight 0.6 instead of 1.0, the leaf
|
|
52
|
+
penalty dropped from 1.5 to 0.5, unknown `ref_kind` / `provenance` now score at or below the
|
|
53
|
+
weakest known value, and test symbols lose 2.0.
|
|
54
|
+
- Scoring reads the evidence that *reached* a candidate, not only whether it is an anchor.
|
|
55
|
+
Proximity is multiplied by that evidence and the reference kind keeps half its weight
|
|
56
|
+
unconditionally (`3.0 · ref_kind · (0.5 + 0.5 · evidence)`), so a `define` edge off a generic
|
|
57
|
+
name like `Content` is no longer worth what one off the model's best hit is worth.
|
|
58
|
+
- New `w9_kind_prior` (2.0): method/function 1.0, class/struct/enum/property 0.5,
|
|
59
|
+
interface/constructor 0.4. A task describes a change, and the change lands in a body;
|
|
60
|
+
declarations and type shells are where it lives. Without the prior, a constructor reached
|
|
61
|
+
through a `define` edge outscored the anchor itself, and 28% of returned symbols were
|
|
62
|
+
containers against 10% in the diffs being replayed. `demote_redundant_containers` then
|
|
63
|
+
subtracts 1.5 from a container that already has a member in the top `2N`.
|
|
64
|
+
- Explicit-name resolution is discounted by ambiguity: evidence is `1.0 / √matches`, and a name
|
|
65
|
+
matching more than 8 symbols is dropped rather than seeding an anchor per match. A qualified
|
|
66
|
+
`Type.member` reference resolves inside its type and no longer *also* admits the bare tail
|
|
67
|
+
(which handed back the ambiguity the qualifier removed); with the type absent from the graph
|
|
68
|
+
the bare tail is used only while it has at most 3 matches. A member echoing its container's
|
|
69
|
+
name — a C# constructor, a partial-class helper — counts at half the container's evidence.
|
|
70
|
+
Generic member names in PR text (`Content`, `ExternalId`, `IsCancelled`) were the largest
|
|
71
|
+
single source of pool flooding: each resolved to every same-named symbol at full strength and
|
|
72
|
+
each expanded its own callers, referrers and file.
|
|
73
|
+
- Expansion is budgeted and evidence-carrying. It runs only from anchors with strength ≥ 0.3 and
|
|
74
|
+
at most the 20 strongest; weaker anchors stay in the pool at distance 0 unexpanded. Every node
|
|
75
|
+
records `source_strength`, the reaching anchor's strength decayed by 0.6 per hop.
|
|
76
|
+
- Anchor finding filters task text through an English + Turkish stop-word list (function words
|
|
77
|
+
and bug-report filler such as "fix", "missing", "check"), so "the" and "and" no longer produce
|
|
78
|
+
anchors. Only identifier-looking tokens (snake_case, camelCase, digits, ALL_CAPS, backticks,
|
|
79
|
+
`Class.method` tails) resolve as *explicit* symbol names; a plain English word cannot turn
|
|
80
|
+
`Principal.check` into an explicit anchor. The lexical source scores symbols by how many of the
|
|
81
|
+
task's terms they cover, weighted by term rarity, and keeps the best 15 instead of 5 hits per
|
|
82
|
+
term. The semantic source decays evidence hyperbolically with rank, `0.9 / (1 + rank/n)`, so
|
|
83
|
+
the model's #1 is comparable to a moderately ambiguous explicit name and the tail still
|
|
84
|
+
outweighs a lone saturated lexical hit. Rank remains the only model input: a similarity number
|
|
85
|
+
would let the model reorder candidates, which is the one thing the engine does not delegate.
|
|
86
|
+
- Test symbols (`tests/`, `test_*.py`, `*_test.go`, `*.test.ts`, `*Test.java`, ...) are excluded
|
|
87
|
+
from the lexical and semantic anchor sources; the automatic semantic anchor over-fetches and
|
|
88
|
+
keeps the first thirty non-test neighbours. `explicit` and `history` still admit them, and
|
|
89
|
+
`find_anchors(include_tests=True)` disables the filter.
|
|
90
|
+
- Same-file sibling expansion runs only from anchors with strength ≥ 0.7, nearest by line, at
|
|
91
|
+
most 12 per anchor, and never from a container — whose members already arrive through the
|
|
92
|
+
graph — so a weak single-term hit no longer drags its whole file into the pool.
|
|
93
|
+
- Retrieval is a single pass. Task signals are computed for every expanded candidate (with
|
|
94
|
+
partial credit for snake/camel parts, so `diff_command` lights up for "diff command"), rather
|
|
95
|
+
than only for the preliminary top `4 · N` — which, being all anchors, meant no neighbour could
|
|
96
|
+
ever receive one. Candidate features come from `GraphRepository.symbol_features`, five chunked
|
|
97
|
+
queries per 500 symbols instead of four round-trips per symbol.
|
|
98
|
+
- Narrowing builds the answer one slot at a time from two streams - the embedding model's ranks
|
|
99
|
+
and the engine's score order - and returns it in selection order, so `narrow(pool, n)` is
|
|
100
|
+
exactly the first `n` of `narrow(pool, n + 1)` (K-monotonic: a K=20 run can be cut to K=10,
|
|
101
|
+
and asking for more can no longer lose a symbol that K=10 had, which the proportional
|
|
102
|
+
`ceil(N · share)` budgets did - #856 in the 40-PR replay went from 100 % at K=10 to 50 % at
|
|
103
|
+
K=20). Position 1 is always the model's #1. Slot `i` goes to the model's next rank while fewer
|
|
104
|
+
than `i · 0.5` picks came from the model; an engine pick that is itself one of the model's
|
|
105
|
+
ranks counts toward that share, because the reserve exists to keep the model's ranks in the
|
|
106
|
+
answer and an agreed pick does exactly that (without this the streams alternated even when
|
|
107
|
+
they agreed and the engine's own hits sat at positions 10-14 instead of 5). Model picks are
|
|
108
|
+
exempt from the diversity caps; engine picks are capped at `ceil(i · 0.6)` per file and
|
|
109
|
+
`ceil(i · 0.3)` per container, containers overall at `ceil(i · 0.2)`. Two gates were tried on
|
|
110
|
+
the tune split and dropped: a score margin an engine pick had to clear over the model's next
|
|
111
|
+
rank (inert) and pre-emption of slot 1 by strong explicit anchors (a long PR description
|
|
112
|
+
names dozens of identifiers, so "explicit" is cheap there). The planned neighbour reserve was
|
|
113
|
+
dropped earlier for the same reason: evidence-scaled scoring already lets a real neighbour
|
|
114
|
+
outrank a weak anchor, and reserving slots for the best-scoring neighbour cost recall.
|
|
115
|
+
- Scoring is `scoring-v4`: `w10_semantic_rank` (1.0) adds `1 / (1 + rank)` for the model's
|
|
116
|
+
ranked list as an explicit factor, and `w11_churn` (1.0) adds a log-saturated prior from the
|
|
117
|
+
number of commits that touched the symbol's file in the last 500 (`file_churn`, filled by the
|
|
118
|
+
indexer from `git log` at the indexed commit - ancestors only, so a PR-base snapshot never sees
|
|
119
|
+
the PR's own commits). Diffs land where diffs have landed before; on the replay corpus most
|
|
120
|
+
indexed files have no commit in the window at all and a handful of hub files have 20-90, so
|
|
121
|
+
the prior separates the live part of a codebase from its sediment. The prior saturates at 20
|
|
122
|
+
commits.
|
|
123
|
+
- The semantic anchor's strength decays with the model's *absolute* rank,
|
|
124
|
+
`0.9 / (1 + rank / 30)`, instead of `rank / len(list)`, so widening the pool no longer makes
|
|
125
|
+
its tail stronger (rank 29 of a 30-list used to be worth as much as rank 10 of an 11-list),
|
|
126
|
+
and expansion is gated by the model's rank rather than by the list length. Steeper decays
|
|
127
|
+
(8 / 10 / 15) were measured and lost recall@20 and semantic retention.
|
|
128
|
+
- Full-text search tokenises identifiers (migration `0009_fts_identifier_parts`): the
|
|
129
|
+
`symbol_fts.document` is a weighted `tsvector` of the exact name (A), its camelCase /
|
|
130
|
+
snake_case parts (B), the container and file-stem words (C) and the signature / docstring
|
|
131
|
+
(D), so "meeting join" matches `MeetingJoinService` and a class name lends weight to its
|
|
132
|
+
members. Task-text tokenisation is Unicode-aware - Turkish words used to be split at the
|
|
133
|
+
first non-ASCII letter - with `İ` folded to `i`, so the Turkish stop-words finally apply, and
|
|
134
|
+
a Turkish -> English domain vocabulary (generic product / software words: "toplantı" ->
|
|
135
|
+
`meeting`, "takvim" -> `calendar`, "bildirim" -> `notification`, ...; suffixed forms are
|
|
136
|
+
recognised by a Turkish suffix-chain check so `serial`, `listener`, `indirect` are left
|
|
137
|
+
alone) feeds the lexical channel and the task-signal feature. No repository-specific names.
|
|
138
|
+
- `GraphRepository.symbol_features` reads degree and the caller / callee flags from the edge
|
|
139
|
+
label tables in SQL (`<graph>._ag_label_edge`, `<graph>."CALLS"`, both btree-indexed on
|
|
140
|
+
`start_id` / `end_id`) instead of Cypher. `MATCH (s)-[r]->()` expands the untyped pattern over
|
|
141
|
+
every edge x vertex label pair and re-plans it per `UNWIND` row: about 1 s of fixed cost for
|
|
142
|
+
an empty id list and 15-30 s for a 400-symbol pool, which was the entire `candidates` stage
|
|
143
|
+
(median 1 s, p90 15 s in the 40-PR replay). The SQL form measures 20-40 ms for the same pool
|
|
144
|
+
with identical counts; Cypher remains the fallback for clients without a raw connection.
|
|
145
|
+
- The embedder defers `embed_fragment` calls and flushes them in batches of up to 500 texts /
|
|
146
|
+
400k characters across files (`Embedder(defer=True)`, the indexer's default), so a run makes
|
|
147
|
+
a few dozen Voyage requests instead of one per file.
|
|
148
|
+
- Graph reads are batched: `callers_of`, `callees_of`, `referrers_of`, `supertypes_of`,
|
|
149
|
+
`subtypes_of`, `symbols_meta`, `symbols_in_files`, `repos_of_symbols`, `resolve_symbols` and
|
|
150
|
+
`lexical_search_many` all take id/term lists, and expansion consumes them frontier by
|
|
151
|
+
frontier (`src/bce/core/orchestrator/bulk.py`, which falls back to the per-symbol methods).
|
|
152
|
+
The bulk queries use `UNWIND $ids AS sid MATCH (s:Symbol {symbol_id: sid})` rather than
|
|
153
|
+
`WHERE s.symbol_id IN $ids`, which on Apache AGE measured about ten times faster. Together
|
|
154
|
+
with the expansion budget this took a long PR description from ~48 s to under 2 s.
|
|
155
|
+
- The confidence level is computed from anchor *agreement* — strong / corroborated anchors,
|
|
156
|
+
whether the top candidate is one, the share of weak single-source anchors and of test code in
|
|
157
|
+
the selection — instead of counting source tags across all anchors, which reported a pack of
|
|
158
|
+
eight noise anchors as `high`. Coverage gains `strong_anchor_count`,
|
|
159
|
+
`corroborated_anchor_count`, `anchor_agreement_ratio`, `weak_anchor_ratio`,
|
|
160
|
+
`max_anchor_strength`, `test_ratio` and `pool_size`.
|
|
161
|
+
- Context items carry `docstring`, `body`, `is_anchor`, `anchor_strength` and `is_test`;
|
|
162
|
+
bodies are loaded for the selected candidates only (`GraphRepository.get_symbol_detail`).
|
|
163
|
+
|
|
164
|
+
### Fixed
|
|
165
|
+
|
|
166
|
+
- The assembler treated `graph_distance == 0` as missing (`0 or 3`), so anchors were always
|
|
167
|
+
rendered reference-only instead of at full detail.
|
|
168
|
+
- Indexing slowed down quadratically with graph size. AGE creates a label table on its first
|
|
169
|
+
`MERGE` with only a primary key, so every edge upsert's `MATCH (a {gid: ...})` was a sequential
|
|
170
|
+
scan of the growing vertex tables (~24 ms per edge on a 15k-symbol graph, most of a 40-minute
|
|
171
|
+
index). Migration `0008_graph_label_indexes` now creates the vertex labels up front with a GIN
|
|
172
|
+
index on `properties` (`fastupdate = off`), which brings the lookup to ~2 ms and makes the
|
|
173
|
+
embedding calls the remaining cost.
|
|
174
|
+
- `VoyageEncoder` sent a whole file's symbols in one request; files with more than 1000 symbols
|
|
175
|
+
(or over the token ceiling) failed the index with `InvalidRequestError`. Requests are now split
|
|
176
|
+
into batches of at most 500 texts / 400k characters.
|
|
177
|
+
|
|
178
|
+
## [0.2.1] - 2026-09-11
|
|
179
|
+
|
|
180
|
+
### Added
|
|
181
|
+
|
|
182
|
+
- Indexing over MCP: `index_repo` and `reindex_repo`, plus the read-only `get_index_job` and
|
|
183
|
+
`list_index_jobs`. Both write tools take a local working tree and enqueue a job rather than
|
|
184
|
+
indexing inline, because a full index outlives any agent's tool-call timeout. They are gated
|
|
185
|
+
behind `BCE_MCP_ALLOW_WRITE` (default off) and are neither advertised nor dispatchable while
|
|
186
|
+
it is off. With it on, the MCP process also runs job workers, so queued indexing progresses
|
|
187
|
+
without a separate `bce serve`; the pool claims `index` and `reindex` only, so a clone that
|
|
188
|
+
REST enqueued is not run inside an editor-spawned process. Cloning a remote repository is
|
|
189
|
+
deliberately not exposed over MCP: it carries its own credentials and stays on REST and the CLI.
|
|
190
|
+
- `JobWorkerPool(job_types=...)` and `claim_next_job(job_types=...)` restrict a pool to a subset
|
|
191
|
+
of the queue.
|
|
192
|
+
- `bce --env-file PATH` and `BCE_ENV_FILE` load configuration from an explicit `.env`. Editors
|
|
193
|
+
spawn `bce serve-mcp` from their own working directory, so a bare `.env` was never found and the
|
|
194
|
+
MCP server silently ran on defaults — no Voyage key, no database password. `serve` and
|
|
195
|
+
`serve-mcp` accept the option after the command name as well as before it, because that is how
|
|
196
|
+
an MCP config or a service definition is written.
|
|
197
|
+
|
|
198
|
+
### Changed
|
|
199
|
+
|
|
200
|
+
- The MCP catalog is now eight read-only tools instead of fourteen. Removed `semantic_search`,
|
|
201
|
+
`find_similar_code`, `find_implementers`, `get_type_hierarchy`, `get_dependencies`,
|
|
202
|
+
`suggest_change_sites`, `select_repos` and `assemble_context`: an agent picks worse when
|
|
203
|
+
handed a dozen overlapping retrieval tools, and `hybrid_search` and `get_context_for_task`
|
|
204
|
+
already cover what they did. All of them remain available over REST.
|
|
205
|
+
- The optional `mcp` extra is now `mcp>=1.0,<2`. SDK 2.x dropped `Server.list_tools` /
|
|
206
|
+
`Server.call_tool`, so `bce serve-mcp` failed on a default `pip install` of 2.x.
|
|
207
|
+
|
|
208
|
+
### Fixed
|
|
209
|
+
|
|
210
|
+
- The first search on an MCP server no longer hangs. Building the embedding encoder imports
|
|
211
|
+
numpy, and loading that extension lazily from inside a tool call stalled for over three
|
|
212
|
+
minutes on Windows while the same import costs seconds at process start, so the encoder is
|
|
213
|
+
now built during startup. Tool dispatch also moved off the event loop onto a worker thread,
|
|
214
|
+
since its database and HTTP calls are synchronous.
|
|
215
|
+
- The MCP server configures logging, against stderr because stdout carries the protocol. Its job
|
|
216
|
+
worker's failures previously fell through to Python's last-resort handler and surfaced in editor
|
|
217
|
+
logs as bare tracebacks; they are now the same JSON lines the API server emits, and they reach
|
|
218
|
+
the log file too. `setup_logging(stream=...)` makes the console destination overridable.
|
|
219
|
+
|
|
220
|
+
## [0.2.0] - 2026-09-10
|
|
221
|
+
|
|
222
|
+
### Changed
|
|
223
|
+
|
|
224
|
+
- Both READMEs now reference images, videos and sibling documents by absolute URL. PyPI
|
|
225
|
+
renders the long description outside the repository, so relative paths resolved to
|
|
226
|
+
nothing there: the banner was a broken image and every documentation link a dead one.
|
|
227
|
+
- Selecting `BCE_EMBEDDING_PROVIDER=voyage` without its API key no longer falls back to the
|
|
228
|
+
hashing encoder. Both that case and a missing `voyageai` package now raise
|
|
229
|
+
`EncoderConfigError`, which the CLI reports as a single line and the REST API as `503`.
|
|
230
|
+
A substituted encoder is not detectable by nearest-neighbour search, so it returned bad
|
|
231
|
+
anchors instead of an error. An unrecognised provider name is rejected the same way.
|
|
232
|
+
- Removed `Settings.voyage_ready`, whose "ready" no longer matched how the encoder is
|
|
233
|
+
chosen.
|
|
234
|
+
|
|
235
|
+
## [0.1.0] - 2026-09-08
|
|
236
|
+
|
|
237
|
+
First public release.
|
|
238
|
+
|
|
239
|
+
### Added
|
|
240
|
+
|
|
241
|
+
- Deterministic retrieval pipeline: anchor discovery, graph expansion, scoring, narrowing
|
|
242
|
+
and context assembly, with coverage and confidence reporting.
|
|
243
|
+
- Code graph over PostgreSQL with Apache AGE and pgvector in a single database. Layer-3
|
|
244
|
+
results are filtered per user against the `scopes` table, resolved from the
|
|
245
|
+
`X-BCE-User` header.
|
|
246
|
+
- Tree-sitter indexing for Python, JavaScript and TypeScript, with optional grammars for
|
|
247
|
+
Java, C# and Go. Incremental re-indexing driven by `git diff`.
|
|
248
|
+
- Layer 1, 2 and 3 tool surfaces exposed over both REST and MCP.
|
|
249
|
+
- Web interface at `/ui`: a graph explorer and a player that replays each pipeline stage
|
|
250
|
+
for a given task. Ships inside the wheel; English by default with a Turkish catalog.
|
|
251
|
+
- `bce` command line interface: `migrate`, `index`, `index-remote`, `reindex`, `bench`,
|
|
252
|
+
`resolve-symbol`, `find-references`, `languages`, `serve` and `serve-mcp`.
|
|
253
|
+
- Benchmark harness reporting latency, recall, precision and determinism.
|
|
254
|
+
- Docker Compose deployment with PostgreSQL, Apache AGE and pgvector preconfigured.
|
|
255
|
+
- `server.json` manifest describing the stdio server for the official MCP registry.
|
|
256
|
+
|
|
257
|
+
[Unreleased]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.2...HEAD
|
|
258
|
+
[0.2.2]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.1...v0.2.2
|
|
259
|
+
[0.2.1]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.2.0...v0.2.1
|
|
260
|
+
[0.2.0]: https://github.com/bgts-ai-org/bgts-context-engine/compare/v0.1.0...v0.2.0
|
|
261
|
+
[0.1.0]: https://github.com/bgts-ai-org/bgts-context-engine/releases/tag/v0.1.0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Stage-by-stage trace of the get_context_for_task retrieval pipeline (UI layer only).
|
|
2
2
|
|
|
3
3
|
Replays the exact flow of :func:`bce.tools.layer3.orchestration.get_context_for_task` - same
|
|
4
|
-
functions, same order, same
|
|
5
|
-
|
|
4
|
+
functions, same order, same single-pass scoring - but records every intermediate stage so a
|
|
5
|
+
frontend can animate the retrieval on the code graph:
|
|
6
6
|
|
|
7
7
|
semantic -> anchors -> expand -> score -> narrow -> assemble
|
|
8
8
|
|
|
@@ -20,22 +20,16 @@ from bce.core.assembler import assemble
|
|
|
20
20
|
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
|
-
from bce.core.orchestrator.orchestrator import RetrievalResult
|
|
24
|
-
from bce.core.
|
|
23
|
+
from bce.core.orchestrator.orchestrator import RetrievalResult, narrow
|
|
24
|
+
from bce.core.orchestrator.text import content_tokens
|
|
25
|
+
from bce.core.scoring.engine import ScoreWeights, score_candidates
|
|
25
26
|
from bce.storage.graph.repository import GraphRepository
|
|
26
27
|
from bce.storage.vector.store import VectorStore
|
|
27
|
-
from bce.tools.layer3.orchestration import
|
|
28
|
-
_auto_semantic_candidates,
|
|
29
|
-
_enrich_items,
|
|
30
|
-
_task_signals,
|
|
31
|
-
)
|
|
28
|
+
from bce.tools.layer3.orchestration import _auto_semantic_candidates, _enrich_items
|
|
32
29
|
|
|
33
30
|
#: Symbol-id chunk size for the bulk metadata Cypher query.
|
|
34
31
|
_META_CHUNK = 500
|
|
35
32
|
|
|
36
|
-
#: Mirrors the prelim-pass multiplier in get_context_for_task (max_candidates * 4).
|
|
37
|
-
_PRELIM_FACTOR = 4
|
|
38
|
-
|
|
39
33
|
|
|
40
34
|
def _elapsed_ms(start: float) -> float:
|
|
41
35
|
return round((time.perf_counter() - start) * 1000, 2)
|
|
@@ -60,7 +54,7 @@ def trace_context_for_task(
|
|
|
60
54
|
semantic_error: str | None = None
|
|
61
55
|
if auto_semantic and store is not None:
|
|
62
56
|
try:
|
|
63
|
-
semantic_candidates = _auto_semantic_candidates(store, task_text, repo_ids)
|
|
57
|
+
semantic_candidates = _auto_semantic_candidates(store, task_text, repo_ids, repository)
|
|
64
58
|
except Exception as exc: # e.g. embeddings table empty / provider unavailable
|
|
65
59
|
semantic_candidates = []
|
|
66
60
|
semantic_error = str(exc)
|
|
@@ -73,7 +67,7 @@ def trace_context_for_task(
|
|
|
73
67
|
stage["error"] = semantic_error
|
|
74
68
|
stages.append(stage)
|
|
75
69
|
|
|
76
|
-
# Stage 1: multi-source anchors (spec section 6.2).
|
|
70
|
+
# Stage 1: multi-source anchors (spec section 6.2), with evidence strength per anchor.
|
|
77
71
|
t = time.perf_counter()
|
|
78
72
|
anchors = find_anchors(
|
|
79
73
|
repository,
|
|
@@ -90,13 +84,14 @@ def trace_context_for_task(
|
|
|
90
84
|
"stage": "anchors",
|
|
91
85
|
"duration_ms": _elapsed_ms(t),
|
|
92
86
|
"anchors": {sid: list(sources) for sid, sources in sorted(anchors.anchors.items())},
|
|
87
|
+
"strength": {sid: anchors.strength_of(sid) for sid in anchors.anchor_ids},
|
|
88
|
+
"task_tokens": content_tokens(task_text),
|
|
93
89
|
}
|
|
94
90
|
)
|
|
95
91
|
|
|
96
|
-
# Stage 2: deterministic expansion (section 6.3).
|
|
97
|
-
# passes - expand_from_anchors is deterministic, so this equals the real double retrieve.
|
|
92
|
+
# Stage 2: deterministic expansion (section 6.3).
|
|
98
93
|
t = time.perf_counter()
|
|
99
|
-
expansion = expand_from_anchors(repository, anchors
|
|
94
|
+
expansion = expand_from_anchors(repository, anchors)
|
|
100
95
|
stages.append(
|
|
101
96
|
{
|
|
102
97
|
"stage": "expand",
|
|
@@ -106,6 +101,7 @@ def trace_context_for_task(
|
|
|
106
101
|
"symbol_id": info.symbol_id,
|
|
107
102
|
"distance": info.distance,
|
|
108
103
|
"is_anchor": info.is_anchor,
|
|
104
|
+
"anchor_strength": info.anchor_strength,
|
|
109
105
|
"ref_kind": info.ref_kind,
|
|
110
106
|
"provenance": info.provenance,
|
|
111
107
|
}
|
|
@@ -114,31 +110,13 @@ def trace_context_for_task(
|
|
|
114
110
|
}
|
|
115
111
|
)
|
|
116
112
|
|
|
117
|
-
# Stage 3: scoring (section 6.4),
|
|
118
|
-
#
|
|
119
|
-
# The second pass reuses the first pass's graph features (degree/leaf/ref_kind do not depend
|
|
120
|
-
# on task signals), so the result is identical to a second to_candidates() at half the cost.
|
|
113
|
+
# Stage 3: scoring (section 6.4), single pass exactly like get_context_for_task: features
|
|
114
|
+
# (degree/leaf/test) and task signals are attached to every candidate by to_candidates.
|
|
121
115
|
weights = ScoreWeights()
|
|
122
116
|
t = time.perf_counter()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
signals = _task_signals(task_text, [c.symbol_id for c in prelim_top], repository)
|
|
127
|
-
final_candidates = [
|
|
128
|
-
Candidate(
|
|
129
|
-
symbol_id=c.symbol_id,
|
|
130
|
-
repo_id=c.repo_id,
|
|
131
|
-
ref_kind=c.ref_kind,
|
|
132
|
-
provenance=c.provenance,
|
|
133
|
-
graph_distance=c.graph_distance,
|
|
134
|
-
degree=c.degree,
|
|
135
|
-
is_leaf=c.is_leaf,
|
|
136
|
-
task_signal=signals.get(c.symbol_id, 0.0),
|
|
137
|
-
anchor=c.anchor,
|
|
138
|
-
)
|
|
139
|
-
for c in base_candidates
|
|
140
|
-
]
|
|
141
|
-
ranked = score_candidates(final_candidates, weights)
|
|
117
|
+
candidates = to_candidates(repository, expansion, task_text=task_text)
|
|
118
|
+
ranked = score_candidates(candidates, weights)
|
|
119
|
+
signals = {c.symbol_id: c.task_signal for c in ranked if c.task_signal > 0.0}
|
|
142
120
|
stages.append(
|
|
143
121
|
{
|
|
144
122
|
"stage": "score",
|
|
@@ -151,6 +129,7 @@ def trace_context_for_task(
|
|
|
151
129
|
"score": c.score,
|
|
152
130
|
"graph_distance": c.graph_distance,
|
|
153
131
|
"is_anchor": c.anchor,
|
|
132
|
+
"is_test": c.is_test,
|
|
154
133
|
"features": c.features,
|
|
155
134
|
}
|
|
156
135
|
for c in ranked
|
|
@@ -158,12 +137,13 @@ def trace_context_for_task(
|
|
|
158
137
|
}
|
|
159
138
|
)
|
|
160
139
|
|
|
161
|
-
# Stage 4: narrowing to top-N (the "1000 -> 8" step).
|
|
162
|
-
|
|
140
|
+
# Stage 4: diversity-aware narrowing to top-N (the "1000 -> 8" step).
|
|
141
|
+
t = time.perf_counter()
|
|
142
|
+
narrowed = narrow(ranked, max_candidates)
|
|
163
143
|
stages.append(
|
|
164
144
|
{
|
|
165
145
|
"stage": "narrow",
|
|
166
|
-
"duration_ms":
|
|
146
|
+
"duration_ms": _elapsed_ms(t),
|
|
167
147
|
"selected": [
|
|
168
148
|
{"symbol_id": c.symbol_id, "score": c.score, "rank": i + 1}
|
|
169
149
|
for i, c in enumerate(narrowed)
|
|
@@ -173,10 +153,14 @@ def trace_context_for_task(
|
|
|
173
153
|
|
|
174
154
|
# Stage 5: token-budget assembly + coverage, same helpers as the real endpoint.
|
|
175
155
|
t = time.perf_counter()
|
|
176
|
-
items = _enrich_items(repository, narrowed)
|
|
156
|
+
items = _enrich_items(repository, narrowed, with_body=True)
|
|
177
157
|
package = assemble(items, max_tokens=max_tokens)
|
|
178
158
|
result = RetrievalResult(
|
|
179
|
-
commit=None,
|
|
159
|
+
commit=None,
|
|
160
|
+
anchors=anchors,
|
|
161
|
+
candidates=narrowed,
|
|
162
|
+
task_signals=signals,
|
|
163
|
+
pool_size=len(ranked),
|
|
180
164
|
)
|
|
181
165
|
coverage = compute_coverage(result, repository)
|
|
182
166
|
stages.append(
|
|
@@ -197,73 +181,6 @@ def trace_context_for_task(
|
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
|
|
200
|
-
def _fast_to_candidates(repository: GraphRepository, expansion: dict) -> list[Candidate]:
|
|
201
|
-
"""Bulk-query equivalent of :func:`bce.core.orchestrator.expand.to_candidates`.
|
|
202
|
-
|
|
203
|
-
``to_candidates`` issues 4 Cypher round-trips per symbol (degree x2, callers, callees), which
|
|
204
|
-
is minutes for a broad expansion. This computes the exact same ``degree``/``is_leaf`` features
|
|
205
|
-
with chunked bulk queries. Falls back to the core function when no real client is available
|
|
206
|
-
(unit-test fakes), keeping behaviour identical either way.
|
|
207
|
-
"""
|
|
208
|
-
client = getattr(repository, "client", None)
|
|
209
|
-
if client is None or not hasattr(client, "cypher"):
|
|
210
|
-
return to_candidates(repository, expansion)
|
|
211
|
-
|
|
212
|
-
ids = sorted(expansion)
|
|
213
|
-
out_deg: dict[str, int] = dict.fromkeys(ids, 0)
|
|
214
|
-
in_deg: dict[str, int] = dict.fromkeys(ids, 0)
|
|
215
|
-
has_callees: set[str] = set()
|
|
216
|
-
has_callers: set[str] = set()
|
|
217
|
-
|
|
218
|
-
for i in range(0, len(ids), _META_CHUNK):
|
|
219
|
-
chunk = ids[i : i + _META_CHUNK]
|
|
220
|
-
params = {"ids": chunk}
|
|
221
|
-
# Any-type edge degree, matching GraphRepository.symbol_degree (out + in).
|
|
222
|
-
for (sid,) in client.cypher(
|
|
223
|
-
"MATCH (s:Symbol)-[r]->() WHERE s.symbol_id IN $ids RETURN s.symbol_id",
|
|
224
|
-
params,
|
|
225
|
-
["sid"],
|
|
226
|
-
):
|
|
227
|
-
out_deg[sid] = out_deg.get(sid, 0) + 1
|
|
228
|
-
for (sid,) in client.cypher(
|
|
229
|
-
"MATCH ()-[r]->(s:Symbol) WHERE s.symbol_id IN $ids RETURN s.symbol_id",
|
|
230
|
-
params,
|
|
231
|
-
["sid"],
|
|
232
|
-
):
|
|
233
|
-
in_deg[sid] = in_deg.get(sid, 0) + 1
|
|
234
|
-
# CALLS presence for the is_leaf feature (leaf = has callers but no callees).
|
|
235
|
-
for (sid,) in client.cypher(
|
|
236
|
-
"MATCH (s:Symbol)-[r:CALLS]->(c:Symbol) WHERE s.symbol_id IN $ids RETURN s.symbol_id",
|
|
237
|
-
params,
|
|
238
|
-
["sid"],
|
|
239
|
-
):
|
|
240
|
-
has_callees.add(sid)
|
|
241
|
-
for (sid,) in client.cypher(
|
|
242
|
-
"MATCH (c:Symbol)-[r:CALLS]->(s:Symbol) WHERE s.symbol_id IN $ids RETURN s.symbol_id",
|
|
243
|
-
params,
|
|
244
|
-
["sid"],
|
|
245
|
-
):
|
|
246
|
-
has_callers.add(sid)
|
|
247
|
-
|
|
248
|
-
candidates: list[Candidate] = []
|
|
249
|
-
for sid in ids:
|
|
250
|
-
info = expansion[sid]
|
|
251
|
-
candidates.append(
|
|
252
|
-
Candidate(
|
|
253
|
-
symbol_id=sid,
|
|
254
|
-
repo_id=info.repo_id,
|
|
255
|
-
ref_kind=info.ref_kind,
|
|
256
|
-
provenance=info.provenance,
|
|
257
|
-
graph_distance=info.distance,
|
|
258
|
-
degree=out_deg.get(sid, 0) + in_deg.get(sid, 0),
|
|
259
|
-
is_leaf=sid not in has_callees and sid in has_callers,
|
|
260
|
-
task_signal=0.0,
|
|
261
|
-
anchor=info.is_anchor,
|
|
262
|
-
)
|
|
263
|
-
)
|
|
264
|
-
return candidates
|
|
265
|
-
|
|
266
|
-
|
|
267
184
|
def _symbol_metadata(repository: GraphRepository, symbol_ids: Any) -> dict[str, dict[str, Any]]:
|
|
268
185
|
"""name/kind/file/line per trace symbol, so the frontend can label and place ghost nodes.
|
|
269
186
|
|