symgrep-codesearch 0.6.2__tar.gz → 0.6.4__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.6.2 → symgrep_codesearch-0.6.4}/PKG-INFO +1 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/pyproject.toml +10 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/cli.py +60 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/enclosing.py +7 -4
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/mcp_server.py +29 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/PKG-INFO +1 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/SOURCES.txt +1 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_blocks_extraction.py +2 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_blocks_retrieval.py +2 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_data_symbols.py +2 -1
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_file_structure.py +3 -2
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_index_reconstruct.py +2 -1
- symgrep_codesearch-0.6.4/tests/test_nudge_classify.py +147 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/LICENSE +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/README.md +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/setup.cfg +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/__init__.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/blocks.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/classfile.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/data_symbols.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/docsymbols.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/mcp_structured.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/report.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/resolve.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/resolve_js.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/resolve_python.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/ripgrep.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/semantic.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/structural.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep/structure.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/dependency_links.txt +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/entry_points.txt +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/requires.txt +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/top_level.txt +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_bulk_describe.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_classfile.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_describe_lane_confinement.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_doc_symbols.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_freshness.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_honest_recall.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_http_ensure.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_index_scope.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_index_worker.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_mcp_structured.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_name_uniqueness.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_nudge_runaway.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_report_filters.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_resolve.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_resolve_js.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_resolve_python.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_resolve_r2.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_review_regressions.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_semantic_freshness_signal.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_sites_and_symbols.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_structural.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_symbol_vocabulary.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_ts_fallback_loud.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_ts_symbol_coverage.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/tests/test_usage.py +0 -0
- {symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/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.6.
|
|
3
|
+
Version: 0.6.4
|
|
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,15 @@ 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.6.4: grammar fix — C# (.cs) now parses. The tree-sitter-language-pack migration left C# pointing at the
|
|
11
|
+
# legacy grammar name "c_sharp"; the maintained pack's manifest calls it "csharp", so `get_parser("c_sharp")`
|
|
12
|
+
# raised TreeSitterUnavailable and every .cs file silently degraded to nearest_decl. enclosing._TS_PACK_NAME now
|
|
13
|
+
# maps c_sharp→csharp (same mechanism as embedded_template→embeddedtemplate). Patch: a pip-installed user on
|
|
14
|
+
# 0.6.3 gets real C# treesitter spans again. (Also lands this release's dev-only work: the one-shot nudge-cost
|
|
15
|
+
# fix and a CI import fix — neither ships in the wheel.)
|
|
16
|
+
# 0.6.3: MCP surface completeness — TOOL_DESCRIPTION now documents structural=/lang= (the last undescribed
|
|
17
|
+
# params), and a new `symgrep install-mcp` subcommand self-registers the server via `claude mcp add`. No API
|
|
18
|
+
# change; keeps PyPI current with the capability descriptions pip-installed MCP clients see.
|
|
10
19
|
# 0.6.2: ripgrep is a system binary and cannot be a normal pip dependency, but the new `[rg]` extra pulls
|
|
11
20
|
# `ripgrep-bin` (a wheel bundling the rg binary) for a self-contained install; rg_binary() now also finds an
|
|
12
21
|
# rg next to the interpreter (venv bin), so the pip-only path works even under `.venv/bin/python -m …`.
|
|
@@ -17,7 +26,7 @@ name = "symgrep-codesearch"
|
|
|
17
26
|
# semantic search. Minor bump (dependency surface changed; no breaking import/CLI/MCP change).
|
|
18
27
|
# 0.5.0: added the public `file_structure` library API (+ its MCP tool and `symgrep structure` CLI), import
|
|
19
28
|
# module-name normalization, and the FileStructure `connects` list (structure schema v2).
|
|
20
|
-
version = "0.6.
|
|
29
|
+
version = "0.6.4"
|
|
21
30
|
description = "Code search with honest recall — structure-aware grep that tells you what it does not know, plus search by meaning"
|
|
22
31
|
readme = "README.md"
|
|
23
32
|
# 3.11+ : the maintained grammar pack (tree-sitter-language-pack, a CORE dep below) ships abi3 wheels for
|
|
@@ -409,10 +409,70 @@ def _cmd_structure(rest) -> int:
|
|
|
409
409
|
return 0
|
|
410
410
|
|
|
411
411
|
|
|
412
|
+
def _resolve_mcp_entrypoint() -> "Optional[str]":
|
|
413
|
+
"""Absolute path to symgrep's MCP stdio entrypoint for THIS install, resolved — never hardcoded. The
|
|
414
|
+
`symgrep-mcp` console script sits next to the interpreter (venv bin) for a pip OR editable install; a bare
|
|
415
|
+
checkout with no install falls back to the repo's bin/symgrep-mcp wrapper. None if neither is found."""
|
|
416
|
+
import shutil
|
|
417
|
+
exe_dir = os.path.dirname(sys.executable or "")
|
|
418
|
+
cand = os.path.join(exe_dir, "symgrep-mcp")
|
|
419
|
+
if os.path.isfile(cand) and os.access(cand, os.X_OK):
|
|
420
|
+
return cand
|
|
421
|
+
on_path = shutil.which("symgrep-mcp")
|
|
422
|
+
if on_path:
|
|
423
|
+
return on_path
|
|
424
|
+
repo_bin = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
|
425
|
+
"bin", "symgrep-mcp")
|
|
426
|
+
if os.path.isfile(repo_bin) and os.access(repo_bin, os.X_OK):
|
|
427
|
+
return repo_bin
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _cmd_install_mcp(rest) -> int:
|
|
432
|
+
"""`symgrep install-mcp [--scope user|local|project] [--name NAME] [--dry-run]` — register symgrep's own
|
|
433
|
+
MCP server with Claude Code, so a fresh install advertises itself as a tool. It does NOT write
|
|
434
|
+
~/.claude.json itself (one shared file must have ONE writer); it shells to `claude mcp add`, the safe
|
|
435
|
+
single writer that ships with the client — so this command carries no config-mutation logic to drift."""
|
|
436
|
+
import shutil
|
|
437
|
+
import subprocess
|
|
438
|
+
sp = argparse.ArgumentParser(prog="symgrep install-mcp")
|
|
439
|
+
sp.add_argument("--scope", default="user", choices=["local", "user", "project"],
|
|
440
|
+
help="Claude Code config scope to register in (default: user)")
|
|
441
|
+
sp.add_argument("--name", default="symgrep", help="server name to register under (default: symgrep)")
|
|
442
|
+
sp.add_argument("--dry-run", action="store_true", help="print the exact `claude mcp add` command, write nothing")
|
|
443
|
+
a = sp.parse_args(rest)
|
|
444
|
+
claude = shutil.which("claude")
|
|
445
|
+
if not claude:
|
|
446
|
+
print("install-mcp: the `claude` CLI is not on PATH. symgrep delegates registration to it rather than "
|
|
447
|
+
"writing ~/.claude.json itself — install Claude Code, then re-run.", file=sys.stderr)
|
|
448
|
+
return 2
|
|
449
|
+
entry = _resolve_mcp_entrypoint()
|
|
450
|
+
if not entry:
|
|
451
|
+
print("install-mcp: could not locate symgrep's MCP entrypoint (`symgrep-mcp` console script or "
|
|
452
|
+
"bin/symgrep-mcp). Install symgrep in this interpreter (`pip install symgrep-codesearch`) first.",
|
|
453
|
+
file=sys.stderr)
|
|
454
|
+
return 2
|
|
455
|
+
cmd = [claude, "mcp", "add", "--scope", a.scope, a.name, "--", entry]
|
|
456
|
+
if a.dry_run:
|
|
457
|
+
print("DRY RUN — would run:\n " + " ".join(cmd))
|
|
458
|
+
return 0
|
|
459
|
+
print("registering symgrep's MCP server:\n " + " ".join(cmd))
|
|
460
|
+
rc = subprocess.run(cmd).returncode
|
|
461
|
+
if rc == 0:
|
|
462
|
+
# MCP servers are read at client START: a just-registered server is callable in the NEXT session, not
|
|
463
|
+
# this one — say so rather than imply a live pickup this cannot deliver.
|
|
464
|
+
print(f"registered '{a.name}' at scope '{a.scope}'. It becomes callable in the NEXT Claude session "
|
|
465
|
+
"(MCP servers load at client start). `claude mcp add` leaves an existing entry intact, so this "
|
|
466
|
+
"is safe to re-run.")
|
|
467
|
+
return rc
|
|
468
|
+
|
|
469
|
+
|
|
412
470
|
def main(argv=None) -> int:
|
|
413
471
|
_argv = list(sys.argv[1:] if argv is None else argv)
|
|
414
472
|
if _argv and _argv[0] == "structure": # `symgrep structure <path>` — a subcommand over the flag CLI
|
|
415
473
|
return _cmd_structure(_argv[1:])
|
|
474
|
+
if _argv and _argv[0] == "install-mcp": # `symgrep install-mcp` — self-register as an MCP server
|
|
475
|
+
return _cmd_install_mcp(_argv[1:])
|
|
416
476
|
ap = argparse.ArgumentParser(prog="symgrep", description=__doc__)
|
|
417
477
|
ap.add_argument("pattern", nargs="?", default=None,
|
|
418
478
|
help="regex to search (omit with --index: then every positional is a ROOT)")
|
|
@@ -213,10 +213,13 @@ _TS_UNAVAILABLE_ANNOUNCED: List[bool] = [False]
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
# symgrep's internal grammar name -> the MAINTAINED pack's (tree-sitter-language-pack) name, ONLY where they
|
|
216
|
-
# differ.
|
|
217
|
-
# ERB/EJS grammar is "embedded_template" in the legacy pack but "embeddedtemplate" in the
|
|
218
|
-
#
|
|
219
|
-
|
|
216
|
+
# differ. Most code languages (TS_LANG_BY_EXT) and "html" resolve identically in both packs; the two that
|
|
217
|
+
# differ: the embedded ERB/EJS grammar is "embedded_template" in the legacy pack but "embeddedtemplate" in the
|
|
218
|
+
# maintained one, and C# is "c_sharp" in the legacy pack but "csharp" in the maintained pack's manifest
|
|
219
|
+
# (requesting "c_sharp" there routes to a download manifest that lacks it → DownloadError, which is why every CI
|
|
220
|
+
# run went red on C# after the pack migration). A name absent here passes through unchanged. This is a fixed
|
|
221
|
+
# spelling map (parsing, not a judgement).
|
|
222
|
+
_TS_PACK_NAME = {"embedded_template": "embeddedtemplate", "c_sharp": "csharp"}
|
|
220
223
|
|
|
221
224
|
|
|
222
225
|
def _ts_parser(lang: str):
|
|
@@ -55,7 +55,30 @@ TOOL_DESCRIPTION = (
|
|
|
55
55
|
"nothing is CALLED that, never that nothing DOES it. That gap is how duplicate implementations get "
|
|
56
56
|
"written — measured in one codebase: `share.scrub` beside `share._scrub_text`, `adapters.call_complete` "
|
|
57
57
|
"beside `adapters.call`, `_save_state` in three files. Requires an index (symgrep --index <root>); if "
|
|
58
|
-
"there is none it says so rather than returning an empty result
|
|
58
|
+
"there is none it says so rather than returning an empty result.\n\n"
|
|
59
|
+
"THREE MORE MODES, each a different question:\n"
|
|
60
|
+
" usage=true — is this name DEFINED, REFERENCED, both, or neither? Returns a verdict in\n"
|
|
61
|
+
" {defined_referenced, defined_unused, undefined_referenced, absent}. LEXICAL,\n"
|
|
62
|
+
" by name: 'defined_unused' is a possibly-unused HINT, never proof of dead code.\n"
|
|
63
|
+
" resolve=true — cross-file candidate resolution: {resolved_unique, ambiguous, external, absent}.\n"
|
|
64
|
+
" When >1 definition shares a name it reports AMBIGUOUS with candidates rather\n"
|
|
65
|
+
" than picking a winner. Use this before trusting any single definition of a\n"
|
|
66
|
+
" name that occurs more than once — import order decides it at runtime.\n"
|
|
67
|
+
" structured=true — machine-readable JSON with a mechanical `status` (ok | no_index | unavailable)\n"
|
|
68
|
+
" instead of prose. Use whenever a program, not a human, reads the result.\n\n"
|
|
69
|
+
"unit= sets --semantic GRANULARITY: 'symbol' (default, a whole function) | 'block' (a sub-function\n"
|
|
70
|
+
"AST block — Type-4 clone retrieval, finds copy-pasted fragments whole-function search misses) |\n"
|
|
71
|
+
"'data' (a schema/config/proto/sql definition) | 'both'/'all'. block and data hits need\n"
|
|
72
|
+
"structured=true to read their span. kind= filters within a unit, e.g. 'data_schema', 'sql_table',\n"
|
|
73
|
+
"'proto_message'.\n\n"
|
|
74
|
+
"structural=true SWITCHES FROM REGEX TO AST-PATTERN MATCH — `pattern` becomes a code snippet with\n"
|
|
75
|
+
"metavariables ($NAME matches one node, $$$ is variadic), matched by syntactic SHAPE, so it finds forms a\n"
|
|
76
|
+
"regex cannot express (e.g. `$A == $A` for self-comparisons) without tripping on whitespace or naming. It\n"
|
|
77
|
+
"REQUIRES lang= to pick the grammar the pattern is parsed in (lang= one of the tree-sitter languages:\n"
|
|
78
|
+
"'python' | 'go' | 'javascript' | 'typescript' | 'tsx' | 'rust' | 'java' | 'c' | 'cpp' | 'ruby' | 'php' |\n"
|
|
79
|
+
"'c_sharp' | 'kotlin' | 'scala' | 'bash'); with no usable grammar it reports UNAVAILABLE rather than\n"
|
|
80
|
+
"silently falling back to text. Use it for structural queries a name/regex search cannot express; use\n"
|
|
81
|
+
"plain regex or semantic=true for everything else."
|
|
59
82
|
)
|
|
60
83
|
|
|
61
84
|
|
|
@@ -131,6 +154,11 @@ def _run_fastmcp(http=None) -> bool:
|
|
|
131
154
|
definitions, references, resolution_basis:"candidate-name"}. verdict ∈ resolved_unique | ambiguous |
|
|
132
155
|
external | absent. When >1 definition shares the name it is AMBIGUOUS (candidates, not one winner).
|
|
133
156
|
|
|
157
|
+
structural=True: match by AST SHAPE, not regex — `pattern` is a code snippet with metavariables
|
|
158
|
+
($NAME matches one node, $$$ is variadic). Requires lang= (the tree-sitter grammar the pattern parses
|
|
159
|
+
in, e.g. "python"/"go"/"rust"); reports UNAVAILABLE when no grammar is usable rather than falling back
|
|
160
|
+
to text. lang is used ONLY by structural search.
|
|
161
|
+
|
|
134
162
|
unit: --semantic granularity — "symbol" (default; a whole function/method, the only pre-v6 behaviour,
|
|
135
163
|
byte-stable) | "block" (a sub-function AST block, Type-4 clone retrieval) | "data" (a schema/data/config
|
|
136
164
|
symbol — a data_schema file unit or a proto/sql definition) | "both"/"all". block/data hits need
|
{symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/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.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -11,8 +11,9 @@ import tempfile
|
|
|
11
11
|
import pathlib
|
|
12
12
|
|
|
13
13
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # tests dir on path: checklib imports when run directly (python tests/<file>.py), as CI does
|
|
14
15
|
from symgrep import blocks, structural # noqa: E402
|
|
15
|
-
from
|
|
16
|
+
from checklib import Checks # noqa: E402
|
|
16
17
|
|
|
17
18
|
c = Checks("test_blocks_extraction")
|
|
18
19
|
|
|
@@ -15,7 +15,8 @@ import tempfile
|
|
|
15
15
|
import pathlib
|
|
16
16
|
|
|
17
17
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
18
|
-
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # tests dir on path: checklib imports when run directly (python tests/<file>.py), as CI does
|
|
19
|
+
from checklib import Checks # noqa: E402
|
|
19
20
|
|
|
20
21
|
c = Checks("test_blocks_retrieval")
|
|
21
22
|
|
|
@@ -12,8 +12,9 @@ import tempfile
|
|
|
12
12
|
import pathlib
|
|
13
13
|
|
|
14
14
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # tests dir on path: checklib imports when run directly (python tests/<file>.py), as CI does
|
|
15
16
|
from symgrep import data_symbols as ds # noqa: E402
|
|
16
|
-
from
|
|
17
|
+
from checklib import Checks # noqa: E402
|
|
17
18
|
|
|
18
19
|
c = Checks("test_data_symbols")
|
|
19
20
|
_SEM_LINE = re.compile(r"^\s+(\d\.\d+)\s+(\S+)\s+(~?\S+):(\d+)\s*$") # honestreview's exact parse
|
|
@@ -16,8 +16,9 @@ import pathlib
|
|
|
16
16
|
import contextlib
|
|
17
17
|
|
|
18
18
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
19
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # tests dir on path: checklib imports when run directly (python tests/<file>.py), as CI does
|
|
19
20
|
from symgrep.structure import file_structure, STRUCTURE_SCHEMA_VERSION # noqa: E402
|
|
20
|
-
from
|
|
21
|
+
from checklib import Checks # noqa: E402
|
|
21
22
|
|
|
22
23
|
c = Checks("test_file_structure")
|
|
23
24
|
|
|
@@ -121,7 +122,7 @@ c.check("CLI == library API (no drift)", rc == 0 and cli_out == s.to_dict())
|
|
|
121
122
|
# 6. a tree-sitter language (Go). tree-sitter is an OPTIONAL dependency: assert the contract that actually
|
|
122
123
|
# holds here — extraction when the grammar is present, LANGUAGE-NAMED honest degradation when it is not
|
|
123
124
|
# (never a silent basis downgrade dressed as a clean result). Both are real contracts, so neither branch skips.
|
|
124
|
-
from
|
|
125
|
+
from checklib import ts_available
|
|
125
126
|
HAVE_TS = ts_available("go") # symgrep's own loader (either grammar pack), not one pack's import name
|
|
126
127
|
|
|
127
128
|
GO = pathlib.Path(d) / "m.go"
|
|
@@ -12,8 +12,9 @@ import sys
|
|
|
12
12
|
import tempfile
|
|
13
13
|
|
|
14
14
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # tests dir on path: checklib imports when run directly (python tests/<file>.py), as CI does
|
|
15
16
|
from symgrep import semantic # noqa: E402
|
|
16
|
-
from
|
|
17
|
+
from checklib import Checks # noqa: E402
|
|
17
18
|
|
|
18
19
|
c = Checks("test_index_reconstruct")
|
|
19
20
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Guards for the nudge's COST DELIVERY — the fix for the measured $70/day defect (2026-10-07).
|
|
2
|
+
|
|
3
|
+
The classification itself is correct and must stay agentic; what these pin is HOW the verdict is fetched, so
|
|
4
|
+
the expensive path can never come back silently:
|
|
5
|
+
|
|
6
|
+
1. NEVER a Claude Code session / the capped Anthropic plan. The old hook shelled `spendguard ask … --budget 0`,
|
|
7
|
+
which routed to the claude-code subscription lane — a full `claude -p` session (~80K cold tokens, Opus).
|
|
8
|
+
Tests: the hook's miss-path shells the one-shot CLASSIFIER (not `spendguard ask`/`--budget 0`); the model is
|
|
9
|
+
non-anthropic (cannot reach the claude-code lane); and the classifier REFUSES a claude-code executor outright.
|
|
10
|
+
2. STAYS AGENTIC. With no gated LLM path available the verdict is None (fail open) — there is NO mechanical
|
|
11
|
+
string/regex classifier that decides true/false. The WHOLE command is the evidence (never sliced).
|
|
12
|
+
3. CACHE IN FRONT. A repeat command returns from cache without spawning anything.
|
|
13
|
+
|
|
14
|
+
Run: python tests/test_nudge_classify.py (command is `python …`, so the LIVE hook sees no grep)
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import types
|
|
20
|
+
|
|
21
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
sys.path.insert(0, HERE)
|
|
23
|
+
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "hooks"))
|
|
24
|
+
from checklib import Checks # noqa: E402
|
|
25
|
+
_t = Checks("test_nudge_classify")
|
|
26
|
+
check = _t.check
|
|
27
|
+
|
|
28
|
+
import symgrep_nudge as h # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---- 1a. NON-ANTHROPIC model: the classifier can never reach the claude-code lane / the capped plan ----------
|
|
32
|
+
import symgrep_nudge_classify as clf # noqa: E402
|
|
33
|
+
check("classifier default model is NON-anthropic (cannot route to the claude-code session / capped plan)",
|
|
34
|
+
not clf.NUDGE_VENDOR_DEFAULT.lower().startswith("anthropic"),
|
|
35
|
+
f"NUDGE_VENDOR_DEFAULT={clf.NUDGE_VENDOR_DEFAULT!r}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---- 1b. The hook's miss-path shells the CLASSIFIER, never `spendguard ask … --budget 0` (the session path) --
|
|
39
|
+
_captured = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _fake_run(argv, **kw):
|
|
43
|
+
_captured["argv"] = list(argv)
|
|
44
|
+
_captured["input"] = kw.get("input")
|
|
45
|
+
out = types.SimpleNamespace()
|
|
46
|
+
out.stdout = json.dumps({"code_search": True, "why": "test", "executor": "api", "cost": 1e-5})
|
|
47
|
+
out.stderr = ""
|
|
48
|
+
out.returncode = 0
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _with(tmp_cache, py_exists=True):
|
|
53
|
+
"""Point the cache at a throwaway sqlite and the classifier python at a real (any) existing file."""
|
|
54
|
+
os.environ["SYMGREP_NUDGE_CACHE"] = tmp_cache
|
|
55
|
+
os.environ["SYMGREP_NUDGE_PYTHON"] = sys.executable if py_exists else os.path.join(tmp_cache, "nope")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
import tempfile # noqa: E402
|
|
59
|
+
_tmp = tempfile.mkdtemp(prefix="nudge-test-")
|
|
60
|
+
_cache = os.path.join(_tmp, "verdicts.sqlite")
|
|
61
|
+
|
|
62
|
+
_orig_run = h.subprocess.run
|
|
63
|
+
h.subprocess.run = _fake_run
|
|
64
|
+
try:
|
|
65
|
+
_with(_cache)
|
|
66
|
+
v = h.judge_is_code_search("rg -n uniquepattern_abc123 src/")
|
|
67
|
+
argv = _captured.get("argv", [])
|
|
68
|
+
check("miss-path verdict is the classifier's JSON (True here)", v is True, f"v={v!r}")
|
|
69
|
+
check("miss-path shells a PYTHON interpreter, not the spendguard binary",
|
|
70
|
+
bool(argv) and "python" in os.path.basename(argv[0]).lower(), f"argv0={argv[:1]}")
|
|
71
|
+
check("miss-path runs symgrep_nudge_classify.py (the one-shot), not `ask`",
|
|
72
|
+
len(argv) >= 2 and argv[1].endswith("symgrep_nudge_classify.py"), f"argv1={argv[1:2]}")
|
|
73
|
+
check("miss-path argv contains NEITHER 'ask' NOR '--budget' (the old session path is gone)",
|
|
74
|
+
"ask" not in argv and "--budget" not in argv, f"argv={argv}")
|
|
75
|
+
check("the WHOLE command is sent to the classifier on stdin (no slice)",
|
|
76
|
+
_captured.get("input") == "rg -n uniquepattern_abc123 src/", f"input={_captured.get('input')!r}")
|
|
77
|
+
|
|
78
|
+
# ---- 3. CACHE IN FRONT: a repeat returns from cache WITHOUT spawning the classifier -----------------------
|
|
79
|
+
_captured.clear()
|
|
80
|
+
|
|
81
|
+
def _boom(*a, **k):
|
|
82
|
+
raise AssertionError("cache HIT must not spawn the classifier subprocess")
|
|
83
|
+
|
|
84
|
+
h.subprocess.run = _boom
|
|
85
|
+
v2 = h.judge_is_code_search("rg -n uniquepattern_abc123 src/")
|
|
86
|
+
check("a repeat command is served from the content cache (no subprocess)", v2 is True, f"v2={v2!r}")
|
|
87
|
+
finally:
|
|
88
|
+
h.subprocess.run = _orig_run
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---- 2a. STAYS AGENTIC: with no gated LLM path, there is NO mechanical verdict (fail open = None) -------------
|
|
92
|
+
for k in ("SYMGREP_SPENDGUARD",):
|
|
93
|
+
os.environ.pop(k, None)
|
|
94
|
+
os.environ["SYMGREP_NUDGE_PYTHON"] = os.path.join(_tmp, "does-not-exist")
|
|
95
|
+
os.environ["SYMGREP_NUDGE_CACHE"] = os.path.join(_tmp, "empty.sqlite")
|
|
96
|
+
# A clearly-code-search command: a regex/keyword classifier WOULD answer True here. The honest answer with no
|
|
97
|
+
# LLM reachable is None (fail open) — proving no mechanical path decides the meaning.
|
|
98
|
+
check("no LLM path reachable → judge returns None (no regex/keyword fallback decides it)",
|
|
99
|
+
h.judge_is_code_search("rg -n 'def handler' src/") is None,
|
|
100
|
+
"a non-None verdict would mean a mechanical classifier exists")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---- 1c. The classifier REFUSES a claude-code executor (the session) even if routing ever drifted there -------
|
|
104
|
+
# Inject a fake spendguard so classify() runs with no network/spend: its adapters.call returns a claude-code hit.
|
|
105
|
+
_fake_sg = types.ModuleType("spendguard")
|
|
106
|
+
_fake_adapters = types.ModuleType("spendguard.adapters")
|
|
107
|
+
_prompt_seen = {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _fake_call(model, prompt, **kw):
|
|
111
|
+
_prompt_seen["model"] = model
|
|
112
|
+
_prompt_seen["prompt"] = prompt
|
|
113
|
+
_prompt_seen["kw"] = kw
|
|
114
|
+
return {"executor": "claude-code", "text": '{"code_search": true, "why": "x"}', "cost": 0.0, "error": None}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_fake_adapters.call = _fake_call
|
|
118
|
+
_fake_sg.adapters = _fake_adapters
|
|
119
|
+
_fake_sg.require = lambda *a, **k: None
|
|
120
|
+
sys.modules["spendguard"] = _fake_sg
|
|
121
|
+
sys.modules["spendguard.adapters"] = _fake_adapters
|
|
122
|
+
|
|
123
|
+
res = clf.classify("rg -n foo src/")
|
|
124
|
+
check("classifier REFUSES a claude-code executor (the ~80K Opus session this fix removes)",
|
|
125
|
+
res["code_search"] is None and "claude-code" in (res["error"] or ""),
|
|
126
|
+
f"res={res}")
|
|
127
|
+
check("the refused (session) verdict carries executor='claude-code' for the guard to see",
|
|
128
|
+
res["executor"] == "claude-code", f"executor={res['executor']!r}")
|
|
129
|
+
|
|
130
|
+
# ---- 2b. WHOLE command is the evidence — classify() sends the command verbatim, never a slice ----------------
|
|
131
|
+
long_cmd = "rg -n '" + ("x" * 5000) + "' src/ --glob '*.py'"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _fake_call_ok(model, prompt, **kw):
|
|
135
|
+
_prompt_seen["prompt"] = prompt
|
|
136
|
+
return {"executor": "api", "text": '{"code_search": true, "why": "x"}', "cost": 1e-5, "error": None}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_fake_adapters.call = _fake_call_ok
|
|
140
|
+
res2 = clf.classify(long_cmd)
|
|
141
|
+
check("classify() sends the ENTIRE command to the model (no truncation of the evidence)",
|
|
142
|
+
_prompt_seen.get("prompt") == long_cmd, "prompt was sliced")
|
|
143
|
+
check("classify() does not pass a strict schema= (so the $0 lane stays reachable)",
|
|
144
|
+
"schema" not in _prompt_seen.get("kw", {}) or _prompt_seen["kw"].get("schema") is None,
|
|
145
|
+
f"kw keys={list(_prompt_seen.get('kw', {}))}")
|
|
146
|
+
|
|
147
|
+
_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
|
{symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/requires.txt
RENAMED
|
File without changes
|
{symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/src/symgrep_codesearch.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/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
|
{symgrep_codesearch-0.6.2 → symgrep_codesearch-0.6.4}/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
|