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.
Files changed (131) hide show
  1. bgts_context_engine-0.2.2/CHANGELOG.md +261 -0
  2. {bgts_context_engine-0.2.1/src/bgts_context_engine.egg-info → bgts_context_engine-0.2.2}/PKG-INFO +1 -1
  3. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/__init__.py +1 -1
  4. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/trace.py +29 -112
  5. bgts_context_engine-0.2.2/src/bce/bench/pr_ablation.py +1413 -0
  6. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/bench/runner.py +1 -0
  7. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/cli.py +242 -0
  8. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/assembler/assembler.py +37 -1
  9. bgts_context_engine-0.2.2/src/bce/core/coverage/confidence.py +259 -0
  10. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/catalogs/en.json +3 -3
  11. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/catalogs/tr.json +3 -3
  12. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/orchestrator/__init__.py +4 -2
  13. bgts_context_engine-0.2.2/src/bce/core/orchestrator/anchors.py +336 -0
  14. bgts_context_engine-0.2.2/src/bce/core/orchestrator/bulk.py +117 -0
  15. bgts_context_engine-0.2.2/src/bce/core/orchestrator/expand.py +383 -0
  16. bgts_context_engine-0.2.2/src/bce/core/orchestrator/orchestrator.py +278 -0
  17. bgts_context_engine-0.2.2/src/bce/core/orchestrator/text.py +490 -0
  18. bgts_context_engine-0.2.2/src/bce/core/scoring/engine.py +327 -0
  19. bgts_context_engine-0.2.2/src/bce/indexing/churn.py +122 -0
  20. bgts_context_engine-0.2.2/src/bce/indexing/embedder/embedder.py +130 -0
  21. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/embedder/encoder.py +38 -7
  22. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/indexer.py +37 -2
  23. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/repository.py +401 -0
  24. bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0008_graph_label_indexes.sql +38 -0
  25. bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0009_fts_identifier_parts.sql +55 -0
  26. bgts_context_engine-0.2.2/src/bce/storage/relational/migrations/0010_file_churn.sql +21 -0
  27. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer3/orchestration.py +104 -62
  28. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2/src/bgts_context_engine.egg-info}/PKG-INFO +1 -1
  29. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/SOURCES.txt +7 -0
  30. bgts_context_engine-0.2.1/CHANGELOG.md +0 -92
  31. bgts_context_engine-0.2.1/src/bce/core/coverage/confidence.py +0 -142
  32. bgts_context_engine-0.2.1/src/bce/core/orchestrator/anchors.py +0 -119
  33. bgts_context_engine-0.2.1/src/bce/core/orchestrator/expand.py +0 -167
  34. bgts_context_engine-0.2.1/src/bce/core/orchestrator/orchestrator.py +0 -77
  35. bgts_context_engine-0.2.1/src/bce/core/scoring/engine.py +0 -106
  36. bgts_context_engine-0.2.1/src/bce/indexing/embedder/embedder.py +0 -75
  37. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/LICENSE +0 -0
  38. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/MANIFEST.in +0 -0
  39. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/README.md +0 -0
  40. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/README.tr.md +0 -0
  41. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/pyproject.toml +0 -0
  42. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/setup.cfg +0 -0
  43. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/__init__.py +0 -0
  44. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/__init__.py +0 -0
  45. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/server.py +0 -0
  46. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/mcp/tools.py +0 -0
  47. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/__init__.py +0 -0
  48. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/app.py +0 -0
  49. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/deps.py +0 -0
  50. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/routes.py +0 -0
  51. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/schemas.py +0 -0
  52. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/assets/index-Cc4pUpSE.js +0 -0
  53. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/assets/index-DrLRaTMU.css +0 -0
  54. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/favicon.svg +0 -0
  55. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/static/index.html +0 -0
  56. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/__init__.py +0 -0
  57. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/queries.py +0 -0
  58. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/router.py +0 -0
  59. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/api/rest/ui/schemas.py +0 -0
  60. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/bench/__init__.py +0 -0
  61. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/config.py +0 -0
  62. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/__init__.py +0 -0
  63. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/assembler/__init__.py +0 -0
  64. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/auth/__init__.py +0 -0
  65. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/auth/scope.py +0 -0
  66. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/coverage/__init__.py +0 -0
  67. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/__init__.py +0 -0
  68. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/locale.py +0 -0
  69. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/i18n/translator.py +0 -0
  70. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/logging.py +0 -0
  71. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/core/scoring/__init__.py +0 -0
  72. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/__init__.py +0 -0
  73. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/enums.py +0 -0
  74. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/domain/models.py +0 -0
  75. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/__init__.py +0 -0
  76. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/embedder/__init__.py +0 -0
  77. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/__init__.py +0 -0
  78. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/bridges.py +0 -0
  79. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/designnote.py +0 -0
  80. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/extractor.py +0 -0
  81. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/extractor/routes.py +0 -0
  82. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/__init__.py +0 -0
  83. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/bitbucket.py +0 -0
  84. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/local.py +0 -0
  85. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/gitsync/remote.py +0 -0
  86. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/linker/__init__.py +0 -0
  87. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/linker/linker.py +0 -0
  88. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/__init__.py +0 -0
  89. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/_treesitter.py +0 -0
  90. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/base.py +0 -0
  91. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/__init__.py +0 -0
  92. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/csharp_provider.py +0 -0
  93. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/go_provider.py +0 -0
  94. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/java_provider.py +0 -0
  95. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/jsts_provider.py +0 -0
  96. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/languages/python_provider.py +0 -0
  97. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/registry.py +0 -0
  98. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/scip/__init__.py +0 -0
  99. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/scip/resolver.py +0 -0
  100. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/parser/symbol_id.py +0 -0
  101. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/upserter/__init__.py +0 -0
  102. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/indexing/upserter/upserter.py +0 -0
  103. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/__init__.py +0 -0
  104. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/store.py +0 -0
  105. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/jobs/worker.py +0 -0
  106. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/__init__.py +0 -0
  107. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/__init__.py +0 -0
  108. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/graph/client.py +0 -0
  109. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/__init__.py +0 -0
  110. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/db.py +0 -0
  111. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0001_extensions_graph.sql +0 -0
  112. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0002_relational.sql +0 -0
  113. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0003_vector.sql +0 -0
  114. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0004_rls.sql +0 -0
  115. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0005_embeddings_dim.sql +0 -0
  116. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0006_fts.sql +0 -0
  117. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrations/0007_jobs.sql +0 -0
  118. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/migrator.py +0 -0
  119. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/relational/queries.py +0 -0
  120. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/vector/__init__.py +0 -0
  121. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/storage/vector/store.py +0 -0
  122. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/__init__.py +0 -0
  123. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer1/__init__.py +0 -0
  124. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer1/primitives.py +0 -0
  125. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer2/__init__.py +0 -0
  126. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer2/search.py +0 -0
  127. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bce/tools/layer3/__init__.py +0 -0
  128. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/dependency_links.txt +0 -0
  129. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/entry_points.txt +0 -0
  130. {bgts_context_engine-0.2.1 → bgts_context_engine-0.2.2}/src/bgts_context_engine.egg-info/requires.txt +0 -0
  131. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bgts-context-engine
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -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.1"
8
+ __version__ = "0.2.2"
@@ -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 two-pass task-signal handling - but records every intermediate stage
5
- so a frontend can animate the retrieval on the code graph:
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.scoring.engine import Candidate, ScoreWeights, score_candidates
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). Run once and reused for both scoring
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.anchor_ids)
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), two passes exactly like get_context_for_task:
118
- # prelim (no task signals) -> task signals on prelim top -> final scoring.
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
- base_candidates = _fast_to_candidates(repository, expansion)
124
- prelim_ranked = score_candidates(base_candidates, weights)
125
- prelim_top = prelim_ranked[: max_candidates * _PRELIM_FACTOR]
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
- narrowed = ranked[:max_candidates]
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": 0.0,
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, anchors=anchors, candidates=narrowed, task_signals=signals
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