local-codebase-indexer 0.3.1__tar.gz → 0.3.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 (68) hide show
  1. {local_codebase_indexer-0.3.1/local_codebase_indexer.egg-info → local_codebase_indexer-0.3.2}/PKG-INFO +1 -1
  2. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/__init__.py +1 -1
  3. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/query.py +59 -5
  4. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2/local_codebase_indexer.egg-info}/PKG-INFO +1 -1
  5. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/server.json +2 -2
  6. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/tests/test_dataaccess.py +30 -0
  7. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/AGENTS.md +0 -0
  8. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/CLAUDE.md +0 -0
  9. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/MANIFEST.in +0 -0
  10. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/README.md +0 -0
  11. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/README.zh-CN.md +0 -0
  12. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/README.zh-TW.md +0 -0
  13. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/TODO.md +0 -0
  14. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/README.md +0 -0
  15. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/baseline.json +0 -0
  16. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Auth/LoginService.cs +0 -0
  17. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Controllers/LotController.cs +0 -0
  18. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Controllers/ScrapController.cs +0 -0
  19. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Data/MesDbContext.cs +0 -0
  20. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Legacy/frmChangeLine.frm +0 -0
  21. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Models/DrillBreakEvent.cs +0 -0
  22. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Models/Lot.cs +0 -0
  23. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Models/ScrapRecord.cs +0 -0
  24. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Printing/LabelPrinter.cs +0 -0
  25. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Repositories/LotRepository.cs +0 -0
  26. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Repositories/ScrapRepository.cs +0 -0
  27. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Services/IScrapService.cs +0 -0
  28. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Services/InspectionService.cs +0 -0
  29. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Services/LotService.cs +0 -0
  30. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Services/ScrapService.cs +0 -0
  31. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Sql/MES_Lot.sql +0 -0
  32. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Sql/usp_Inspection_Report.sql +0 -0
  33. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Sql/usp_Lot_UpdateStatus.sql +0 -0
  34. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Sql/usp_Scrap_Insert.sql +0 -0
  35. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Utils/DateRangeHelper.cs +0 -0
  36. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Utils/PartNumberDecoder.cs +0 -0
  37. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Web/scrapForm.ts +0 -0
  38. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/Web.config +0 -0
  39. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/corpus/tools/export_report.py +0 -0
  40. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/queries_stdlib.json +0 -0
  41. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/queries_synthetic.json +0 -0
  42. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/benchmarks/run_benchmark.py +0 -0
  43. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/index.md +0 -0
  44. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/llms.txt +0 -0
  45. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/__main__.py +0 -0
  46. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/callgraph.py +0 -0
  47. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/cli.py +0 -0
  48. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/core.py +0 -0
  49. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/cscalls.py +0 -0
  50. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/csdata.py +0 -0
  51. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/dataflow.py +0 -0
  52. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/db.py +0 -0
  53. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/embedder.py +0 -0
  54. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/langparse.py +0 -0
  55. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/mcp_server.py +0 -0
  56. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/scanner.py +0 -0
  57. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/sqlaccess.py +0 -0
  58. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer/synonyms.py +0 -0
  59. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer.egg-info/SOURCES.txt +0 -0
  60. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer.egg-info/dependency_links.txt +0 -0
  61. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer.egg-info/entry_points.txt +0 -0
  62. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer.egg-info/requires.txt +0 -0
  63. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/local_codebase_indexer.egg-info/top_level.txt +0 -0
  64. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/pyproject.toml +0 -0
  65. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/requirements.txt +0 -0
  66. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/setup.cfg +0 -0
  67. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/tests/test_indexer.py +0 -0
  68. {local_codebase_indexer-0.3.1 → local_codebase_indexer-0.3.2}/tests/test_onnx_plumbing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: local-codebase-indexer
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Local codebase indexer and context provider for AI coding assistants: MCP server and CLI with SQL data-access and C# call relations. No cloud, no server.
5
5
  Author: sfy257000
6
6
  Project-URL: Homepage, https://github.com/sfy257000/local-codebase-indexer
@@ -1,3 +1,3 @@
1
1
  """Universal Local Codebase Indexer & Context Provider (see index.md)."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.3.2"
@@ -94,6 +94,7 @@ class CodebaseTools:
94
94
  terms.append(t)
95
95
  ranked: dict[str, list[int]] = {"keyword": [], "symbol": [], "vector": []}
96
96
  ranks_override: dict[str, dict[int, int]] = {}
97
+ exact_ids: set[int] = set() # chunks whose symbol name contains a full identifier typed in the query
97
98
 
98
99
  # keyword layer: FTS5 / bm25 over chunk text
99
100
  if terms:
@@ -125,6 +126,19 @@ class CodebaseTools:
125
126
  ):
126
127
  if _has_word(t, r["file_path"]):
127
128
  scores[r[0]] += 1
129
+ # a full identifier typed by the user (SFC_MLot) must match names containing it whole, whatever the
130
+ # case or underscores (SFC_Mlot, SFC_MLotRepository); sub-word matches alone would favour every *Lot
131
+ for ident in self._whole_identifiers(query):
132
+ squashed = re.sub(r"[^a-z0-9]", "", ident.lower())
133
+ like = f"%{squashed}%"
134
+ for r in self.conn.execute(
135
+ "SELECT c.rowid FROM chunk_metadata c JOIN symbols s ON s.id = c.symbol_id "
136
+ "WHERE replace(lower(s.symbol_name), '_', '') LIKE ? LIMIT 500", (like,)):
137
+ scores[r[0]] += 3
138
+ exact_ids.add(r[0])
139
+ for r in self.conn.execute(
140
+ "SELECT rowid FROM chunk_metadata WHERE replace(lower(file_path), '_', '') LIKE ? LIMIT 300", (like,)):
141
+ scores[r[0]] += 2
128
142
  ordered = sorted(scores.items(), key=lambda kv: (-kv[1], kv[0]))[:pool]
129
143
  ranked["symbol"] = [rid for rid, _ in ordered]
130
144
  # equal match counts must share a rank, otherwise arbitrary row order decides
@@ -154,13 +168,26 @@ class CodebaseTools:
154
168
  fused[rid] += LAYER_WEIGHTS[name] / (RRF_K + rank)
155
169
  via[rid].append(name)
156
170
  top_ids = [rid for rid, _ in sorted(fused.items(), key=lambda kv: -kv[1])]
171
+ if exact_ids: # a typed identifier beats sub-word noise (every *Lot class), ties keep the fused order
172
+ top_ids = [r for r in top_ids if r in exact_ids] + [r for r in top_ids if r not in exact_ids]
157
173
 
158
174
  results, seen_spans = [], set()
159
- for acc in self._route_data_access(query)[:limit]:
175
+ routed, bare = self._route_data_access(query)
176
+ routed_hits = []
177
+ for acc in routed[:limit]:
160
178
  hit = self._chunk_result(acc)
161
179
  if hit is not None and (hit["file_path"], hit["start_line"], hit["end_line"]) not in seen_spans:
162
180
  seen_spans.add((hit["file_path"], hit["start_line"], hit["end_line"]))
163
- results.append(hit)
181
+ routed_hits.append(hit)
182
+ if bare and routed_hits and top_ids:
183
+ # only a name was typed: the definition (best ordinary match) first, then who touches it
184
+ first = self._ordinary_hit(top_ids[0], fused, via)
185
+ if first is not None:
186
+ span = (first["file_path"], first["start_line"], first["end_line"])
187
+ routed_hits = [h for h in routed_hits if (h["file_path"], h["start_line"], h["end_line"]) != span]
188
+ results.append(first)
189
+ seen_spans.add(span)
190
+ results.extend(routed_hits)
164
191
  for rid in top_ids:
165
192
  if len(results) >= limit:
166
193
  break
@@ -194,14 +221,39 @@ class CodebaseTools:
194
221
  return {"query": query, "results": results, "related_files": self._related_files(results[:3])}
195
222
 
196
223
  # ------------------------------------------------------------------ data access routing
224
+ def _ordinary_hit(self, rid: int, fused: dict, via: dict) -> dict | None:
225
+ row = self.conn.execute(
226
+ "SELECT c.file_path, c.start_line, c.end_line, c.raw_text, s.symbol_type, s.qualified_name, s.signature "
227
+ "FROM chunk_metadata c LEFT JOIN symbols s ON s.id = c.symbol_id WHERE c.rowid=?", (rid,)).fetchone()
228
+ if row is None:
229
+ return None
230
+ lines = textwrap.dedent(row["raw_text"].rstrip()).split("\n")
231
+ return {"file_path": row["file_path"], "start_line": row["start_line"], "end_line": row["end_line"],
232
+ "symbol": row["qualified_name"], "symbol_type": row["symbol_type"], "signature": row["signature"],
233
+ "score": round(fused[rid], 5), "matched_by": via[rid],
234
+ "snippet": "\n".join(lines[:12]) + ("\n..." if len(lines) > 12 else "")}
235
+
236
+ @staticmethod
237
+ def _whole_identifiers(query: str) -> list[str]:
238
+ """Identifier-looking tokens (underscore, digit or camelCase inside) of at least 5 characters."""
239
+ out = []
240
+ for ident in re.findall(r"[A-Za-z_][\w$]*", query):
241
+ if len(ident) >= 5 and (re.search(r"[_\d]", ident[1:]) or
242
+ (re.search(r"[a-z]", ident) and re.search(r"[A-Z]", ident[1:]))):
243
+ out.append(ident)
244
+ return out
245
+
197
246
  _OBJECT_HINT = re.compile(r"\b(table|tables|procedure|procedures|proc|stored|view|views|sp)\b|資料表|預存程序|存储过程|数据表", re.I)
198
247
 
199
- def _route_data_access(self, query: str) -> list[dict]:
248
+ def _route_data_access(self, query: str) -> tuple[list[dict], bool]:
200
249
  """Queries such as "what writes to MES_Lot" name a known table / procedure and an intent. A ranked chunk
201
250
  list cannot enumerate every writer, so those queries get the accessors from the data access index first."""
202
251
  intents = dataflow.detect_intents(query)
252
+ bare = False
203
253
  if not intents:
204
- return []
254
+ if len(query.split()) > 3:
255
+ return [], False
256
+ intents, bare = {"touch"}, True
205
257
  keys = dataflow.known_object_keys(self.conn)
206
258
  hinted = bool(self._OBJECT_HINT.search(query))
207
259
  found: list[str] = []
@@ -218,7 +270,9 @@ class CodebaseTools:
218
270
  if sig not in seen:
219
271
  seen.add(sig)
220
272
  out.append(acc)
221
- return out
273
+ if bare: # leave room for the ordinary name matches (the class, the repository, the file)
274
+ out = out[:3]
275
+ return out, bare
222
276
 
223
277
  def _chunk_result(self, acc: dict) -> dict | None:
224
278
  conn = self.conn
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: local-codebase-indexer
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Local codebase indexer and context provider for AI coding assistants: MCP server and CLI with SQL data-access and C# call relations. No cloud, no server.
5
5
  Author: sfy257000
6
6
  Project-URL: Homepage, https://github.com/sfy257000/local-codebase-indexer
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/sfy257000/local-codebase-indexer",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.3.1",
10
+ "version": "0.3.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "local-codebase-indexer",
15
- "version": "0.3.1",
15
+ "version": "0.3.2",
16
16
  "runtimeHint": "uvx",
17
17
  "transport": {
18
18
  "type": "stdio"
@@ -112,3 +112,33 @@ def test_synonym_expansion_and_project_override(tmp_path):
112
112
  e2 = Expander(str(tmp_path))
113
113
  assert "wpnlot" in e2.expand("在製批 查詢")
114
114
  assert "proc" not in e2.expand("SP list")
115
+
116
+
117
+ def test_bare_table_name_finds_its_repository_not_every_lot_class(tmp_path):
118
+ """Real-world miss: searching SFC_MLot ranked OutsourceLot / PNLToPCSLot above SFC_MLotRepository because the
119
+ sub-word 'lot' matched everything, and the table was spelled SFC_Mlot in the SQL string."""
120
+ (tmp_path / "Repo").mkdir()
121
+ (tmp_path / "Models").mkdir()
122
+ (tmp_path / "Repo" / "SFC_MLotRepository.cs").write_text("""using System.Threading.Tasks;
123
+ using Dapper;
124
+ namespace E {
125
+ public class SFC_MLotRepository : ISFC_MLotRepository {
126
+ private readonly System.Data.IDbConnection _db;
127
+ public Task<int> Deduct(decimal qty) {
128
+ string sql = @" UPDATE SFC_Mlot SET Qty = Qty - @qty WHERE PostFlag = 'W'; DELETE FROM SFC_Mlot WHERE Qty <= 0;";
129
+ return _db.ExecuteAsync(sql, new { qty });
130
+ }
131
+ }
132
+ public interface ISFC_MLotRepository { Task<int> Deduct(decimal qty); }
133
+ }
134
+ """, encoding="utf-8")
135
+ lots = "".join(f"public class Lot{i} : SFCLot {{ }} public class ReworkLot{i} : SFCLot {{ }}\n" for i in range(8))
136
+ (tmp_path / "Models" / "SFCLot.cs").write_text(
137
+ "namespace E { public partial class SFCLot { }\n" + lots + "public class OutsourceLot : SFCLot { } }", encoding="utf-8")
138
+ t = CodebaseTools(str(tmp_path))
139
+ for q in ("SFC_MLot", "SFC_Mlot", "sfc_mlot"):
140
+ top = t.semantic_search(q, limit=5)["results"]
141
+ assert top[0]["file_path"].endswith("SFC_MLotRepository.cs"), q
142
+ assert sum(r["file_path"].endswith("SFC_MLotRepository.cs") for r in top) >= 3, q
143
+ assert any((r.get("data_access") or {}).get("access") == "write" for r in top), q
144
+ t.indexer.close()