symgrep-codesearch 0.7.0__tar.gz → 0.7.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.
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/PKG-INFO +1 -1
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/pyproject.toml +6 -1
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/mcp_server.py +149 -38
- symgrep_codesearch-0.7.2/src/symgrep/runtime_paths.py +26 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/semantic.py +15 -9
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/PKG-INFO +1 -1
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/SOURCES.txt +4 -0
- symgrep_codesearch-0.7.2/tests/test_mcp_error_log.py +56 -0
- symgrep_codesearch-0.7.2/tests/test_mcp_thread_safety.py +57 -0
- symgrep_codesearch-0.7.2/tests/test_mcp_version.py +72 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/LICENSE +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/README.md +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/setup.cfg +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/__init__.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/archives.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/blocks.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/classfile.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/cli.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/data_symbols.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/docsymbols.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/enclosing.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/glob_recall.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/mcp_structured.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/report.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/resolve.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/resolve_js.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/resolve_python.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/ripgrep.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/structural.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep/structure.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/dependency_links.txt +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/entry_points.txt +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/requires.txt +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/top_level.txt +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_blocks_extraction.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_blocks_retrieval.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_bulk_describe.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_classfile.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_data_symbols.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_describe_lane_confinement.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_doc_symbols.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_file_structure.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_freshness.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_honest_recall.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_http_ensure.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_index_reconstruct.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_index_scope.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_index_worker.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_mcp_structured.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_name_uniqueness.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_nudge_classify.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_nudge_runaway.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_report_filters.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_resolve.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_resolve_js.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_resolve_python.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_resolve_r2.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_review_regressions.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_semantic_freshness_signal.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_semantic_glob_lang.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_sites_and_symbols.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_structural.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_symbol_vocabulary.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_ts_fallback_loud.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_ts_symbol_coverage.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_usage.py +0 -0
- {symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_veccore_adoption.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: symgrep-codesearch
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Code search with honest recall — structure-aware grep that tells you what it does not know, plus search by meaning
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/ashdamle/symgrep
|
|
@@ -7,6 +7,11 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# Semgrep team. The IMPORT name is unchanged — `import symgrep` — so consumers (honestreview, spendguard,
|
|
8
8
|
# 7thsense, warden) are unaffected by the distribution rename. The package dir stays src/symgrep.
|
|
9
9
|
name = "symgrep-codesearch"
|
|
10
|
+
# 0.7.1: honesty fix — the MCP daemon now reports its TRUE version. _package_version() looked up the IMPORT
|
|
11
|
+
# name ("symgrep") instead of the DISTRIBUTION name ("symgrep-codesearch"), so importlib.metadata could not
|
|
12
|
+
# find the dist and serverInfo.version silently fell through to "0.0.0+unpackaged" (or matched a stale
|
|
13
|
+
# editable "symgrep" 0.3.0 dist-info) — a daemon running 0.7.0 would not say 0.7.0. Now queries
|
|
14
|
+
# symgrep-codesearch first, then the legacy import name, then the honest fallback; test_mcp_version pins it.
|
|
10
15
|
# 0.6.4: grammar fix — C# (.cs) now parses. The tree-sitter-language-pack migration left C# pointing at the
|
|
11
16
|
# legacy grammar name "c_sharp"; the maintained pack's manifest calls it "csharp", so `get_parser("c_sharp")`
|
|
12
17
|
# raised TreeSitterUnavailable and every .cs file silently degraded to nearest_decl. enclosing._TS_PACK_NAME now
|
|
@@ -26,7 +31,7 @@ name = "symgrep-codesearch"
|
|
|
26
31
|
# semantic search. Minor bump (dependency surface changed; no breaking import/CLI/MCP change).
|
|
27
32
|
# 0.5.0: added the public `file_structure` library API (+ its MCP tool and `symgrep structure` CLI), import
|
|
28
33
|
# module-name normalization, and the FileStructure `connects` list (structure schema v2).
|
|
29
|
-
version = "0.7.
|
|
34
|
+
version = "0.7.2"
|
|
30
35
|
description = "Code search with honest recall — structure-aware grep that tells you what it does not know, plus search by meaning"
|
|
31
36
|
readme = "README.md"
|
|
32
37
|
# 3.11+ : the maintained grammar pack (tree-sitter-language-pack, a CORE dep below) ships abi3 wheels for
|
|
@@ -14,6 +14,8 @@ importing symgrep in-process into a venv that lacks fastembed. Loopback only, by
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
18
|
+
from logging.handlers import RotatingFileHandler
|
|
17
19
|
import os
|
|
18
20
|
import sys
|
|
19
21
|
import traceback
|
|
@@ -21,6 +23,7 @@ from typing import List, Optional
|
|
|
21
23
|
|
|
22
24
|
from .report import DEFAULT_MAX_SITES, render_report
|
|
23
25
|
from .cli import build_report
|
|
26
|
+
from .runtime_paths import resolved_mcp_log_path
|
|
24
27
|
|
|
25
28
|
PROTOCOL_VERSION_FALLBACK = "2024-11-05"
|
|
26
29
|
HTTP_HOST_DEFAULT = "127.0.0.1" # $SYMGREP_MCP_HOST — loopback unless deliberately widened
|
|
@@ -28,16 +31,106 @@ HTTP_HOST_DEFAULT = "127.0.0.1" # $SYMGREP_MCP_HOST — loopback unless d
|
|
|
28
31
|
# engine): 4900 ccwatch · 4910 7thsense · 4911 symgrep. Override via $SYMGREP_MCP_PORT.
|
|
29
32
|
HTTP_PORT_DEFAULT = 4911
|
|
30
33
|
HTTP_PATH = "/mcp" # mirrors ccwatch's POST /mcp convention
|
|
34
|
+
MCP_LOG_MAX_BYTES_DEFAULT = 5 * 1024 * 1024
|
|
35
|
+
MCP_LOG_BACKUP_COUNT_DEFAULT = 3
|
|
36
|
+
MCP_REQUEST_LOGGER_NAME = "symgrep.mcp.requests"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _mcp_request_logger() -> logging.Logger:
|
|
40
|
+
"""Return the rotating request logger, rebinding it when SYMGREP_LOG changes at runtime."""
|
|
41
|
+
log_path = resolved_mcp_log_path()
|
|
42
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
request_logger = logging.getLogger(MCP_REQUEST_LOGGER_NAME)
|
|
44
|
+
request_logger.setLevel(logging.INFO)
|
|
45
|
+
request_logger.propagate = False
|
|
46
|
+
current_path = getattr(request_logger, "_symgrep_log_path", None)
|
|
47
|
+
if current_path != str(log_path):
|
|
48
|
+
for existing_handler in list(request_logger.handlers):
|
|
49
|
+
existing_handler.close()
|
|
50
|
+
request_logger.removeHandler(existing_handler)
|
|
51
|
+
rotating_handler = RotatingFileHandler(
|
|
52
|
+
log_path, maxBytes=int(os.environ.get("SYMGREP_LOG_MAX_BYTES", MCP_LOG_MAX_BYTES_DEFAULT)),
|
|
53
|
+
backupCount=int(os.environ.get("SYMGREP_LOG_BACKUP_COUNT", MCP_LOG_BACKUP_COUNT_DEFAULT)),
|
|
54
|
+
encoding="utf-8")
|
|
55
|
+
rotating_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
|
56
|
+
request_logger.addHandler(rotating_handler)
|
|
57
|
+
request_logger._symgrep_log_path = str(log_path)
|
|
58
|
+
return request_logger
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _summarize_mcp_tool_arguments(tool_arguments: dict) -> str:
|
|
62
|
+
"""Create a compact mechanical summary without dropping the pattern or single-file path."""
|
|
63
|
+
parts = []
|
|
64
|
+
for argument_name in sorted(tool_arguments):
|
|
65
|
+
argument_value = tool_arguments[argument_name]
|
|
66
|
+
if argument_name == "roots" and isinstance(argument_value, list):
|
|
67
|
+
rendered_value = f"{len(argument_value)} root(s)"
|
|
68
|
+
elif argument_value in (None, False, [], ""):
|
|
69
|
+
continue
|
|
70
|
+
else:
|
|
71
|
+
rendered_value = repr(argument_value)
|
|
72
|
+
parts.append(f"{argument_name}={rendered_value}")
|
|
73
|
+
return ", ".join(parts)
|
|
31
74
|
|
|
32
75
|
|
|
33
|
-
def
|
|
34
|
-
"""
|
|
35
|
-
|
|
76
|
+
def _returned_mcp_error_details(tool_result):
|
|
77
|
+
"""Parse only server-authored error fields/prefixes; normal search verdicts are not errors."""
|
|
78
|
+
parsed_result = tool_result
|
|
79
|
+
if isinstance(tool_result, str):
|
|
80
|
+
try:
|
|
81
|
+
parsed_result = json.loads(tool_result)
|
|
82
|
+
except (TypeError, json.JSONDecodeError):
|
|
83
|
+
if tool_result.startswith("semantic search UNAVAILABLE"):
|
|
84
|
+
return "unavailable", tool_result
|
|
85
|
+
return None
|
|
86
|
+
if not isinstance(parsed_result, dict):
|
|
87
|
+
return None
|
|
88
|
+
status = parsed_result.get("status")
|
|
89
|
+
if status in ("unavailable", "no_index"):
|
|
90
|
+
return parsed_result.get("reason") or status, parsed_result.get("message") or repr(parsed_result)
|
|
91
|
+
if "error" in parsed_result:
|
|
92
|
+
error_value = parsed_result["error"]
|
|
93
|
+
if isinstance(error_value, dict):
|
|
94
|
+
return error_value.get("code", "error"), error_value.get("message") or repr(error_value)
|
|
95
|
+
return "error", str(error_value)
|
|
96
|
+
if parsed_result.get("refused") is True:
|
|
97
|
+
return "refused", parsed_result.get("message") or repr(parsed_result)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _invoke_logged_mcp_tool(tool_name: str, tool_arguments: dict, invocation):
|
|
102
|
+
"""Run one tool call and make every raised or returned error durable in the MCP log."""
|
|
103
|
+
argument_summary = _summarize_mcp_tool_arguments(tool_arguments)
|
|
36
104
|
try:
|
|
37
|
-
|
|
38
|
-
return version("symgrep")
|
|
105
|
+
tool_result = invocation()
|
|
39
106
|
except Exception:
|
|
40
|
-
|
|
107
|
+
_mcp_request_logger().exception("tool=%s args={%s} raised", tool_name, argument_summary)
|
|
108
|
+
raise
|
|
109
|
+
returned_error = _returned_mcp_error_details(tool_result)
|
|
110
|
+
if returned_error is not None:
|
|
111
|
+
error_code, error_message = returned_error
|
|
112
|
+
_mcp_request_logger().warning("tool=%s args={%s} returned error code=%s message=%s",
|
|
113
|
+
tool_name, argument_summary, error_code, error_message)
|
|
114
|
+
return tool_result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _package_version() -> str:
|
|
118
|
+
"""The TRUE installed version of this package — serverInfo.version must not lie about what is running.
|
|
119
|
+
|
|
120
|
+
One source of truth: pyproject's version via importlib.metadata (the fallback used to hardcode '0.2.0'
|
|
121
|
+
while the package said 0.3.0). The lookup key is the DISTRIBUTION name, which is NOT the import name:
|
|
122
|
+
you `pip install symgrep-codesearch` but `import symgrep` (the bare `symgrep` PyPI name is parked by the
|
|
123
|
+
Semgrep team). `version("symgrep")` therefore does not resolve to this dist — it fell through to the
|
|
124
|
+
unpackaged fallback, or worse matched a STALE editable `symgrep` dist-info left from an old 0.3.0
|
|
125
|
+
install — so the daemon reported a version that was not the one it was running. Query the real dist name
|
|
126
|
+
first, then the legacy import name (so a pre-rename install still resolves), then an honest fallback."""
|
|
127
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
128
|
+
for dist in ("symgrep-codesearch", "symgrep"):
|
|
129
|
+
try:
|
|
130
|
+
return version(dist)
|
|
131
|
+
except PackageNotFoundError:
|
|
132
|
+
continue
|
|
133
|
+
return "0.0.0+unpackaged"
|
|
41
134
|
|
|
42
135
|
TOOL_DESCRIPTION = (
|
|
43
136
|
"Structure-aware code search — use this INSTEAD of plain Grep when exploring "
|
|
@@ -185,15 +278,24 @@ def _run_fastmcp(http=None) -> bool:
|
|
|
185
278
|
roots: directories/files to search (absolute paths preferred).
|
|
186
279
|
globs: rg --glob filters, e.g. ["*.py"].
|
|
187
280
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
281
|
+
tool_arguments = {"pattern": pattern, "roots": roots, "case_insensitive": case_insensitive,
|
|
282
|
+
"fixed_string": fixed_string, "multiline": multiline, "globs": globs,
|
|
283
|
+
"include_ignored": include_ignored, "max_sites": max_sites, "semantic": semantic,
|
|
284
|
+
"k": k, "json_output": json_output, "structured": structured,
|
|
285
|
+
"structural": structural, "lang": lang, "usage": usage, "resolve": resolve,
|
|
286
|
+
"unit": unit, "kind": kind}
|
|
287
|
+
return _invoke_logged_mcp_tool(
|
|
288
|
+
"search", tool_arguments,
|
|
289
|
+
lambda: _search(pattern, roots, case_insensitive, fixed_string, multiline, globs,
|
|
290
|
+
include_ignored, max_sites, semantic, k, json_output, structured,
|
|
291
|
+
structural, lang, usage, resolve, unit, kind))
|
|
191
292
|
|
|
192
293
|
@mcp.tool(description="Structural symbols of ONE file: every function/class with its line span and "
|
|
193
294
|
"basis (ast/treesitter/…). The AST layer without an in-process import.")
|
|
194
295
|
def file_symbols(path: str) -> str:
|
|
195
296
|
from .mcp_structured import structured_file_symbols
|
|
196
|
-
return
|
|
297
|
+
return _invoke_logged_mcp_tool(
|
|
298
|
+
"file_symbols", {"path": path}, lambda: json.dumps(structured_file_symbols(path)))
|
|
197
299
|
|
|
198
300
|
@mcp.tool(description="Mechanical STRUCTURE of ONE file, from the same grammar parse: {schema_version, "
|
|
199
301
|
"language, basis, functions:[{name,signature,start,end}], classes, imports, "
|
|
@@ -203,13 +305,15 @@ def _run_fastmcp(http=None) -> bool:
|
|
|
203
305
|
"caller's agentic layer).")
|
|
204
306
|
def file_structure(path: str) -> str:
|
|
205
307
|
from .mcp_structured import structured_file_structure
|
|
206
|
-
return
|
|
308
|
+
return _invoke_logged_mcp_tool(
|
|
309
|
+
"file_structure", {"path": path}, lambda: json.dumps(structured_file_structure(path)))
|
|
207
310
|
|
|
208
311
|
@mcp.tool(description="The roots the semantic index covers, so an un-hit query reads as 'not indexed "
|
|
209
312
|
"here' vs 'indexed, nothing near'.")
|
|
210
313
|
def indexed_roots() -> str:
|
|
211
314
|
from .mcp_structured import structured_indexed_roots
|
|
212
|
-
return
|
|
315
|
+
return _invoke_logged_mcp_tool(
|
|
316
|
+
"indexed_roots", {}, lambda: json.dumps(structured_indexed_roots()))
|
|
213
317
|
|
|
214
318
|
if http:
|
|
215
319
|
host, port = http
|
|
@@ -296,26 +400,26 @@ def _fallback_handle(req):
|
|
|
296
400
|
name = params.get("name")
|
|
297
401
|
a = params.get("arguments", {})
|
|
298
402
|
if name == "search":
|
|
299
|
-
text =
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
a.get("structural", False), a.get("lang"),
|
|
308
|
-
a.get("usage", False), a.get("resolve", False),
|
|
309
|
-
a.get("unit", "symbol"), a.get("kind"))
|
|
403
|
+
text = _invoke_logged_mcp_tool(
|
|
404
|
+
name, a, lambda: _search(
|
|
405
|
+
a["pattern"], a.get("roots"), a.get("case_insensitive", False),
|
|
406
|
+
a.get("fixed_string", False), a.get("multiline", False), a.get("globs"),
|
|
407
|
+
a.get("include_ignored", False), a.get("max_sites", DEFAULT_MAX_SITES),
|
|
408
|
+
a.get("semantic", False), a.get("k", 8), a.get("json_output", False),
|
|
409
|
+
a.get("structured", False), a.get("structural", False), a.get("lang"),
|
|
410
|
+
a.get("usage", False), a.get("resolve", False), a.get("unit", "symbol"), a.get("kind")))
|
|
310
411
|
elif name == "file_symbols":
|
|
311
412
|
from .mcp_structured import structured_file_symbols
|
|
312
|
-
text =
|
|
413
|
+
text = _invoke_logged_mcp_tool(
|
|
414
|
+
name, a, lambda: json.dumps(structured_file_symbols(a["path"])))
|
|
313
415
|
elif name == "file_structure":
|
|
314
416
|
from .mcp_structured import structured_file_structure
|
|
315
|
-
text =
|
|
417
|
+
text = _invoke_logged_mcp_tool(
|
|
418
|
+
name, a, lambda: json.dumps(structured_file_structure(a["path"])))
|
|
316
419
|
elif name == "indexed_roots":
|
|
317
420
|
from .mcp_structured import structured_indexed_roots
|
|
318
|
-
text =
|
|
421
|
+
text = _invoke_logged_mcp_tool(
|
|
422
|
+
name, a, lambda: json.dumps(structured_indexed_roots()))
|
|
319
423
|
else:
|
|
320
424
|
raise ValueError(f"unknown tool {name!r}")
|
|
321
425
|
return {"content": [{"type": "text", "text": text}]}
|
|
@@ -334,12 +438,16 @@ def _run_fallback() -> None:
|
|
|
334
438
|
except json.JSONDecodeError:
|
|
335
439
|
# JSON-RPC 2.0: a parse error is answered (-32700), not dropped — a client that sent
|
|
336
440
|
# malformed JSON and hears nothing waits forever.
|
|
337
|
-
|
|
338
|
-
|
|
441
|
+
parse_error = {"code": -32700, "message": "parse error"}
|
|
442
|
+
_mcp_request_logger().warning("tool=jsonrpc args={} returned error code=%s message=%s",
|
|
443
|
+
parse_error["code"], parse_error["message"])
|
|
444
|
+
print(json.dumps({"jsonrpc": "2.0", "id": None, "error": parse_error}), flush=True)
|
|
339
445
|
continue
|
|
340
446
|
if not isinstance(req, dict):
|
|
341
|
-
|
|
342
|
-
|
|
447
|
+
invalid_request_error = {"code": -32600, "message": "invalid request"}
|
|
448
|
+
_mcp_request_logger().warning("tool=jsonrpc args={} returned error code=%s message=%s",
|
|
449
|
+
invalid_request_error["code"], invalid_request_error["message"])
|
|
450
|
+
print(json.dumps({"jsonrpc": "2.0", "id": None, "error": invalid_request_error}), flush=True)
|
|
343
451
|
continue
|
|
344
452
|
rid = req.get("id")
|
|
345
453
|
try:
|
|
@@ -347,17 +455,18 @@ def _run_fallback() -> None:
|
|
|
347
455
|
if result is None and rid is not None:
|
|
348
456
|
# a REQUEST (has id) for a method we don't serve gets -32601, not silence;
|
|
349
457
|
# notifications (no id) legitimately get nothing.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
458
|
+
method_error = {"code": -32601, "message": f"method not found: {req.get('method')!r}"}
|
|
459
|
+
_mcp_request_logger().warning("tool=jsonrpc args={method=%r} returned error code=%s message=%s",
|
|
460
|
+
req.get("method"), method_error["code"], method_error["message"])
|
|
461
|
+
print(json.dumps({"jsonrpc": "2.0", "id": rid, "error": method_error}), flush=True)
|
|
354
462
|
continue
|
|
355
463
|
except Exception as e:
|
|
464
|
+
_mcp_request_logger().exception("tool=jsonrpc args={method=%r} raised", req.get("method"))
|
|
356
465
|
if rid is not None:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
466
|
+
internal_error = {"code": -32603, "message": f"{type(e).__name__}: {e}"}
|
|
467
|
+
_mcp_request_logger().warning("tool=jsonrpc args={method=%r} returned error code=%s message=%s",
|
|
468
|
+
req.get("method"), internal_error["code"], internal_error["message"])
|
|
469
|
+
print(json.dumps({"jsonrpc": "2.0", "id": rid, "error": internal_error}), flush=True)
|
|
361
470
|
continue
|
|
362
471
|
if rid is not None and result is not None:
|
|
363
472
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}),
|
|
@@ -374,6 +483,8 @@ def main() -> None:
|
|
|
374
483
|
sys.exit(2)
|
|
375
484
|
host = os.environ.get("SYMGREP_MCP_HOST", HTTP_HOST_DEFAULT)
|
|
376
485
|
port = int(os.environ.get("SYMGREP_MCP_PORT", HTTP_PORT_DEFAULT))
|
|
486
|
+
_mcp_request_logger() # create the rotating request log before serving
|
|
487
|
+
sys.stderr.write(f"symgrep-mcp: error log {resolved_mcp_log_path()}\n")
|
|
377
488
|
# WARM SHARED DAEMON = SINGLETON (one per port): every session CONNECTS to this warm HTTP server via urllib
|
|
378
489
|
# instead of spawning a stdio copy. A second start detects the incumbent (flock via spendguard's single-instance
|
|
379
490
|
# primitive) and exits 0 — never races FastMCP into a bind crash — so a supervisor can (re)start it idempotently.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared runtime paths used by the server and its lifecycle hooks."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SYMGREP_CACHE_DIRECTORY_ENV = "SYMGREP_CACHE_DIR"
|
|
8
|
+
SYMGREP_LOG_PATH_ENV = "SYMGREP_LOG"
|
|
9
|
+
SYMGREP_CACHE_DIRECTORY_NAME = "symgrep"
|
|
10
|
+
SYMGREP_MCP_LOG_FILENAME = "mcp.log"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolved_symgrep_cache_directory() -> pathlib.Path:
|
|
14
|
+
"""Return symgrep's configured cache directory, using the repository's existing user-cache default."""
|
|
15
|
+
configured = os.environ.get(SYMGREP_CACHE_DIRECTORY_ENV)
|
|
16
|
+
if configured:
|
|
17
|
+
return pathlib.Path(configured).expanduser()
|
|
18
|
+
return pathlib.Path.home() / ".cache" / SYMGREP_CACHE_DIRECTORY_NAME
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolved_mcp_log_path() -> pathlib.Path:
|
|
22
|
+
"""Return the request/crash log path shared by the MCP process and its ensure hook."""
|
|
23
|
+
configured = os.environ.get(SYMGREP_LOG_PATH_ENV)
|
|
24
|
+
if configured:
|
|
25
|
+
return pathlib.Path(configured).expanduser()
|
|
26
|
+
return resolved_symgrep_cache_directory() / SYMGREP_MCP_LOG_FILENAME
|
|
@@ -47,6 +47,8 @@ import sys
|
|
|
47
47
|
import threading
|
|
48
48
|
from typing import List, Optional, Tuple
|
|
49
49
|
|
|
50
|
+
from .runtime_paths import resolved_symgrep_cache_directory
|
|
51
|
+
|
|
50
52
|
MODEL = os.environ.get("SYMGREP_EMBED_MODEL", "BAAI/bge-small-en-v1.5")
|
|
51
53
|
DIM = 384 # bge-small-en-v1.5 geometry; veccore's ensure() guards it (DimMismatch)
|
|
52
54
|
# The model is encoded in the COLLECTION name, so changing SYMGREP_EMBED_MODEL means a NEW collection — never
|
|
@@ -62,7 +64,7 @@ def index_path() -> pathlib.Path:
|
|
|
62
64
|
import, so a test or caller setting it afterwards silently hit the real index (flagged by the seam
|
|
63
65
|
axis as a one-sided `config:INDEX` contract, 2026-08-19)."""
|
|
64
66
|
return pathlib.Path(os.environ.get("SYMGREP_INDEX")
|
|
65
|
-
or (
|
|
67
|
+
or (resolved_symgrep_cache_directory() / "semantic.sqlite"))
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
class Unavailable(RuntimeError):
|
|
@@ -106,15 +108,15 @@ def _veccore_present() -> bool:
|
|
|
106
108
|
return importlib.util.find_spec("veccore") is not None
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
# store+core are cached
|
|
110
|
-
#
|
|
111
|
+
# SQLite-backed store+core pairs are cached per thread and index path, so a daemon worker never reuses a
|
|
112
|
+
# connection created by another worker. Thread-local lifetime also releases a worker's cache when it exits.
|
|
111
113
|
_EMBEDDER = [None]
|
|
112
|
-
|
|
114
|
+
_SEMANTIC_THREAD_STATE = threading.local()
|
|
113
115
|
_CACHE_LOCK = threading.Lock()
|
|
114
116
|
|
|
115
117
|
|
|
116
118
|
def _core_for(path_str):
|
|
117
|
-
"""(store, core) for the index at `path_str`, built once
|
|
119
|
+
"""(store, core) for the index at `path_str`, built once per calling thread under `_CACHE_LOCK`.
|
|
118
120
|
|
|
119
121
|
CACHE STALENESS, explicitly: the cache is PROCESS-LIVED and keyed by the index PATH — every symgrep CLI run
|
|
120
122
|
is a fresh process, so it starts empty. Within one process a store's sqlite connection SEES other
|
|
@@ -124,12 +126,16 @@ def _core_for(path_str):
|
|
|
124
126
|
Tests point $SYMGREP_INDEX at distinct temp paths, so each gets its own entry. The embedder is
|
|
125
127
|
path-independent (a reload per call was catastrophically slow), so it is shared. Maps veccore's store-open
|
|
126
128
|
failure to symgrep's index_unusable; a missing veccore surfaced earlier as Unavailable(no_embedder)."""
|
|
127
|
-
|
|
129
|
+
thread_cores = getattr(_SEMANTIC_THREAD_STATE, "cores_by_index_path", None)
|
|
130
|
+
if thread_cores is None:
|
|
131
|
+
thread_cores = {}
|
|
132
|
+
_SEMANTIC_THREAD_STATE.cores_by_index_path = thread_cores
|
|
133
|
+
hit = thread_cores.get(path_str)
|
|
128
134
|
if hit is not None:
|
|
129
135
|
return hit
|
|
130
136
|
vc = _veccore()
|
|
131
137
|
with _CACHE_LOCK: # one builder wins; a racing caller re-reads the cache
|
|
132
|
-
hit =
|
|
138
|
+
hit = thread_cores.get(path_str)
|
|
133
139
|
if hit is not None:
|
|
134
140
|
return hit
|
|
135
141
|
try:
|
|
@@ -145,8 +151,8 @@ def _core_for(path_str):
|
|
|
145
151
|
core.register(vc.CollectionSpec(name=COLLECTION, backend="sqlite_numpy", embedder=f"fastembed:{MODEL}",
|
|
146
152
|
dim=DIM, payload_schema=("qual", "path", "lineno", "what", "stamp",
|
|
147
153
|
"unit", "parent_key", "block_start", "block_end")))
|
|
148
|
-
|
|
149
|
-
return
|
|
154
|
+
thread_cores[path_str] = (store, core) # sqlite connections stay owned by their worker thread
|
|
155
|
+
return thread_cores[path_str]
|
|
150
156
|
|
|
151
157
|
|
|
152
158
|
def _embedder():
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: symgrep-codesearch
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Code search with honest recall — structure-aware grep that tells you what it does not know, plus search by meaning
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/ashdamle/symgrep
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/SOURCES.txt
RENAMED
|
@@ -17,6 +17,7 @@ src/symgrep/resolve.py
|
|
|
17
17
|
src/symgrep/resolve_js.py
|
|
18
18
|
src/symgrep/resolve_python.py
|
|
19
19
|
src/symgrep/ripgrep.py
|
|
20
|
+
src/symgrep/runtime_paths.py
|
|
20
21
|
src/symgrep/semantic.py
|
|
21
22
|
src/symgrep/structural.py
|
|
22
23
|
src/symgrep/structure.py
|
|
@@ -40,7 +41,10 @@ tests/test_http_ensure.py
|
|
|
40
41
|
tests/test_index_reconstruct.py
|
|
41
42
|
tests/test_index_scope.py
|
|
42
43
|
tests/test_index_worker.py
|
|
44
|
+
tests/test_mcp_error_log.py
|
|
43
45
|
tests/test_mcp_structured.py
|
|
46
|
+
tests/test_mcp_thread_safety.py
|
|
47
|
+
tests/test_mcp_version.py
|
|
44
48
|
tests/test_name_uniqueness.py
|
|
45
49
|
tests/test_nudge_classify.py
|
|
46
50
|
tests/test_nudge_runaway.py
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Regression: MCP refusals and exceptions are durable in the configured rotating error log."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "src"))
|
|
11
|
+
sys.path.insert(0, HERE)
|
|
12
|
+
from checklib import Checks # noqa: E402
|
|
13
|
+
|
|
14
|
+
from symgrep import mcp_server # noqa: E402
|
|
15
|
+
|
|
16
|
+
_t = Checks("test_mcp_error_log")
|
|
17
|
+
check = _t.check
|
|
18
|
+
|
|
19
|
+
temporary_directory = tempfile.TemporaryDirectory(prefix="symgrep-mcp-log-")
|
|
20
|
+
configured_log = pathlib.Path(temporary_directory.name) / "request-errors.log"
|
|
21
|
+
saved_log_path = os.environ.get("SYMGREP_LOG")
|
|
22
|
+
os.environ["SYMGREP_LOG"] = str(configured_log)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
returned_message = "semantic index is unavailable for this regression"
|
|
26
|
+
returned_payload = {"status": "unavailable", "reason": "no_index", "message": returned_message}
|
|
27
|
+
mcp_server._invoke_logged_mcp_tool(
|
|
28
|
+
"search", {"pattern": "find capability", "roots": [temporary_directory.name]},
|
|
29
|
+
lambda: __import__("json").dumps(returned_payload))
|
|
30
|
+
|
|
31
|
+
def raise_request_regression_error():
|
|
32
|
+
raise RuntimeError("request regression exploded")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
mcp_server._invoke_logged_mcp_tool(
|
|
36
|
+
"file_symbols", {"path": pathlib.Path(temporary_directory.name) / "broken.py"},
|
|
37
|
+
raise_request_regression_error)
|
|
38
|
+
except RuntimeError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
for handler in logging.getLogger(mcp_server.MCP_REQUEST_LOGGER_NAME).handlers:
|
|
42
|
+
handler.flush()
|
|
43
|
+
logged = configured_log.read_text(encoding="utf-8")
|
|
44
|
+
check("returned MCP refusal is logged with tool, args, returned code, and exact message",
|
|
45
|
+
"tool=search" in logged and "pattern='find capability'" in logged
|
|
46
|
+
and "code=no_index" in logged and returned_message in logged, logged)
|
|
47
|
+
check("raised MCP exception is logged with its complete traceback",
|
|
48
|
+
"tool=file_symbols" in logged and "Traceback (most recent call last)" in logged
|
|
49
|
+
and "raise_request_regression_error" in logged and "request regression exploded" in logged, logged)
|
|
50
|
+
finally:
|
|
51
|
+
if saved_log_path is None:
|
|
52
|
+
os.environ.pop("SYMGREP_LOG", None)
|
|
53
|
+
else:
|
|
54
|
+
os.environ["SYMGREP_LOG"] = saved_log_path
|
|
55
|
+
|
|
56
|
+
_t.finish()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Regression: semantic daemon workers never share a thread-bound SQLite store."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "src"))
|
|
10
|
+
sys.path.insert(0, HERE)
|
|
11
|
+
from checklib import Checks # noqa: E402
|
|
12
|
+
|
|
13
|
+
from symgrep import semantic # noqa: E402
|
|
14
|
+
|
|
15
|
+
_t = Checks("test_mcp_thread_safety")
|
|
16
|
+
check = _t.check
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
semantic._embedder()
|
|
20
|
+
except semantic.Unavailable:
|
|
21
|
+
_t.skip("veccore/fastembed absent — threaded semantic-store regression not exercised here")
|
|
22
|
+
else:
|
|
23
|
+
temporary_directory = tempfile.TemporaryDirectory(prefix="symgrep-thread-store-")
|
|
24
|
+
index_file = os.path.join(temporary_directory.name, "semantic.sqlite")
|
|
25
|
+
workers_ready = threading.Barrier(2)
|
|
26
|
+
worker_results = []
|
|
27
|
+
worker_failures = []
|
|
28
|
+
result_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
def exercise_thread_owned_semantic_store():
|
|
31
|
+
try:
|
|
32
|
+
first_store, first_core = semantic._core_for(index_file)
|
|
33
|
+
second_store, second_core = semantic._core_for(index_file)
|
|
34
|
+
workers_ready.wait()
|
|
35
|
+
first_store.existing_ids(semantic.COLLECTION)
|
|
36
|
+
with result_lock:
|
|
37
|
+
worker_results.append((threading.get_ident(), first_store, first_core,
|
|
38
|
+
first_store is second_store and first_core is second_core))
|
|
39
|
+
except Exception as error:
|
|
40
|
+
with result_lock:
|
|
41
|
+
worker_failures.append(error)
|
|
42
|
+
|
|
43
|
+
worker_threads = [threading.Thread(target=exercise_thread_owned_semantic_store) for _ in range(2)]
|
|
44
|
+
for worker_thread in worker_threads:
|
|
45
|
+
worker_thread.start()
|
|
46
|
+
for worker_thread in worker_threads:
|
|
47
|
+
worker_thread.join()
|
|
48
|
+
|
|
49
|
+
check("two daemon-style worker threads can use the same semantic index without SQLite thread errors",
|
|
50
|
+
not worker_failures and len(worker_results) == 2,
|
|
51
|
+
"\n".join(f"{type(error).__name__}: {error}" for error in worker_failures))
|
|
52
|
+
check("each worker reuses its own store/core pair within that thread",
|
|
53
|
+
len(worker_results) == 2 and all(result[3] for result in worker_results), str(worker_results))
|
|
54
|
+
check("different worker threads receive different SQLite-backed stores for the same index path",
|
|
55
|
+
len(worker_results) == 2 and worker_results[0][1] is not worker_results[1][1], str(worker_results))
|
|
56
|
+
|
|
57
|
+
_t.finish()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""serverInfo.version must be TRUE — _package_version() reports the real installed version, not a lie.
|
|
2
|
+
|
|
3
|
+
This is an honesty guard, not a cosmetic one. The MCP daemon advertises _package_version() as its
|
|
4
|
+
serverInfo.version (mcp_server._run_fastmcp and _fallback_handle); a consumer reads it to know which engine
|
|
5
|
+
answered. The bug it pins: the DISTRIBUTION name is `symgrep-codesearch` but the lookup used the IMPORT name
|
|
6
|
+
`symgrep`, so importlib.metadata could not find the dist and the daemon fell through to "0.0.0+unpackaged"
|
|
7
|
+
(or matched a STALE editable `symgrep` 0.3.0 dist-info) — reporting a version it was not running.
|
|
8
|
+
|
|
9
|
+
Two claims:
|
|
10
|
+
1. When the package IS installed (CI does `pip install -e .`), _package_version() returns the REAL
|
|
11
|
+
distribution version — never the unpackaged fallback — and that value equals what importlib.metadata
|
|
12
|
+
reports for the installed distribution.
|
|
13
|
+
2. In that editable/source checkout, the reported version MATCHES pyproject's version (the SSOT). Before
|
|
14
|
+
the fix this failed: version("symgrep") → "0.0.0+unpackaged" != pyproject's "0.7.0".
|
|
15
|
+
|
|
16
|
+
Run: python tests/test_mcp_version.py (no pytest dependency, like the CLI itself)
|
|
17
|
+
"""
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import tomllib
|
|
21
|
+
from importlib.metadata import version as _dist_version, PackageNotFoundError
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
24
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
25
|
+
from checklib import Checks # noqa: E402
|
|
26
|
+
_t = Checks("test_mcp_version")
|
|
27
|
+
check = _t.check
|
|
28
|
+
|
|
29
|
+
from symgrep.mcp_server import _package_version # noqa: E402
|
|
30
|
+
|
|
31
|
+
UNPACKAGED = "0.0.0+unpackaged"
|
|
32
|
+
|
|
33
|
+
# pyproject's version is the single source of truth — read it, never hardcode the number here.
|
|
34
|
+
_PYPROJECT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "pyproject.toml")
|
|
35
|
+
with open(_PYPROJECT, "rb") as fh:
|
|
36
|
+
pyproject_version = tomllib.load(fh)["project"]["version"]
|
|
37
|
+
|
|
38
|
+
# The distribution name is `symgrep-codesearch`; the legacy/import name `symgrep` is a different (parked)
|
|
39
|
+
# project and must never be what we report. Discover whether EITHER dist is installed in this interpreter.
|
|
40
|
+
installed_version = None
|
|
41
|
+
installed_dist = None
|
|
42
|
+
for _dist in ("symgrep-codesearch", "symgrep"):
|
|
43
|
+
try:
|
|
44
|
+
installed_version = _dist_version(_dist)
|
|
45
|
+
installed_dist = _dist
|
|
46
|
+
break
|
|
47
|
+
except PackageNotFoundError:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
reported = _package_version()
|
|
51
|
+
|
|
52
|
+
if installed_version is not None:
|
|
53
|
+
# Claim 1: the reported version is the real installed version, not the fallback.
|
|
54
|
+
check("reported version is NOT the unpackaged fallback when the package is installed",
|
|
55
|
+
reported != UNPACKAGED, f"reported={reported!r}")
|
|
56
|
+
check(f"reported version == installed distribution version ({installed_dist})",
|
|
57
|
+
reported == installed_version, f"reported={reported!r} installed={installed_version!r}")
|
|
58
|
+
# Claim 2: an editable/source checkout (CI's `pip install -e .`) advertises pyproject's version. This is
|
|
59
|
+
# the assertion that would have CAUGHT the bug — version('symgrep') returned the fallback, not 0.7.0.
|
|
60
|
+
check("reported version matches pyproject (SSOT) in an editable/source checkout",
|
|
61
|
+
reported == pyproject_version, f"reported={reported!r} pyproject={pyproject_version!r}")
|
|
62
|
+
else:
|
|
63
|
+
# No install in this interpreter (e.g. a bare source run via PYTHONPATH). The installed-version claims
|
|
64
|
+
# cannot be verified here — record a VISIBLE skip so the un-run claim never masquerades as green. CI's
|
|
65
|
+
# `pip install -e .` job is where claims 1 and 2 actually execute.
|
|
66
|
+
_t.skip("neither symgrep-codesearch nor symgrep is installed in this interpreter; "
|
|
67
|
+
"install-version claims not verified (they run under CI's `pip install -e .`)")
|
|
68
|
+
# Still assert the one thing that IS true with no install: the fallback is reached honestly and exactly.
|
|
69
|
+
check("with no installed distribution, the fallback is the honest unpackaged sentinel",
|
|
70
|
+
reported == UNPACKAGED, f"reported={reported!r}")
|
|
71
|
+
|
|
72
|
+
_t.finish()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/requires.txt
RENAMED
|
File without changes
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/src/symgrep_codesearch.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_describe_lane_confinement.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{symgrep_codesearch-0.7.0 → symgrep_codesearch-0.7.2}/tests/test_semantic_freshness_signal.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|