Skip to content

Commit 7f2bb1c

Browse files
jgravelleclaude
andcommitted
release: v1.108.114 - route() mutate + stateful intent buckets
route()/classify_intent had no bucket for edit-commands or session/recent-change queries; both fell through to search_symbols or NO_MATCH (measured 95% NO_MATCH on a 140-request corpus). Add both, on-charter: - stateful -> get_changed_symbols / get_session_context (working-tree delta + session journal). Placed first: stateful phrasings carry trigger words for the impact/reference rules. - mutate -> recognize-and-redirect to the read-only prep tool for the edit kind (check_rename_safe / check_delete_safe / plan_refactoring / check_edit_safe). jcm performs no edit; the agent applies with its own editor. Leading-verb anchored so a question about an edit falls through to the impact rules. No _QUERY_ARG entry, so route(execute=true) never auto-runs a mutation flow. Verified 20/20 mutate + 20/20 stateful on the corpus, zero regression to existing routes. Charter guard test asserts every mutate recommendation is non-state-changing and does not trip the exec/write tripwire. New tests/test_v1_108_114.py (9). No INDEX_VERSION bump; pure routing logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b2163fe commit 7f2bb1c

5 files changed

Lines changed: 184 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.114] - 2026-07-08 - route(): mutate + stateful intent buckets
6+
7+
### Added
8+
9+
- **`route()` now recognizes two intent classes it previously dropped** — edit/execute
10+
commands and session/recent-change queries. Before, both fell through to
11+
`search_symbols` or matched nothing:
12+
- **stateful** — "what did I just change", "uncommitted edits", "different from
13+
main", "recap this session" route to `get_changed_symbols` /
14+
`get_session_context` (the working-tree delta and session journal, not the
15+
whole index). Placed first in the rule order because these phrasings carry
16+
trigger words for the impact/reference rules.
17+
- **mutate** — "rename X", "delete the dead code", "refactor the auth module",
18+
"add a retry" are recognized as edit commands. jcm is read-only by charter and
19+
performs no edit, so route recommends the matching **read-only prep tool**
20+
(`check_rename_safe` / `check_delete_safe` / `plan_refactoring` /
21+
`check_edit_safe`) and the agent applies the change with its own editor.
22+
Anchored to a leading imperative verb, so a *question* about an edit
23+
("what breaks if I rename X") still falls through to the impact rules.
24+
- Mutate recommendations are recognize-and-redirect only — no `_QUERY_ARG` entry,
25+
so `route(execute=true)` never auto-runs a mutation flow from a free-form task.
26+
- New `tests/test_v1_108_114.py` (9). No INDEX_VERSION bump; pure routing logic in
27+
`counter.py`, no schema/tool-count change.
28+
529
## [1.108.113] - 2026-07-07 - New tool: get_architecture_metrics (Gini / Lakos depth / DSM)
630

731
### Added

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4+
- **Version:** 1.108.114 — **`route()` gains mutate + stateful intent buckets (the leakage finding from the router-triage lab).** The Counter's `route()`/`classify_intent` (`counter.py` `_INTENT_RULES`) had no bucket for edit-commands or session/recent-change queries → they fell through to `search_symbols` or NO_MATCH (measured: mutate+stateful 95% NO_MATCH against a 140-request corpus). Added both, on-charter: **stateful** ("what did I just change", "uncommitted edits", "different from main", "recap this session") → `get_changed_symbols` / `get_session_context`; placed FIRST because stateful phrasings carry trigger words for the impact/reference rules ("affected by my recent changes"). **mutate** ("rename X", "delete the dead code", "refactor the auth module", "add a retry") → recognize-and-redirect to the READ-ONLY prep tool for that edit kind (`check_rename_safe`/`check_delete_safe`/`plan_refactoring`/`check_edit_safe`) — jcm performs no edit, the agent applies with its own editor. **Anchored to a LEADING imperative verb** so a QUESTION about an edit ("what breaks if I rename X") falls through to the impact rules, not the mutate bucket. Recognize-only: no `_QUERY_ARG` entry for the prep tools, so `route(execute=true)` never auto-runs a mutation flow. Verified 20/20 mutate + 20/20 stateful on the corpus, zero regression to existing routes. Charter guard test asserts every mutate recommendation is non-state-changing + doesn't trip the exec/write tripwire. New `tests/test_v1_108_114.py` (9). NO INDEX_VERSION bump; pure routing logic, no schema/tool-count change. Origin: `C:\MCPs\request-router-lab\` triage/cross-check (memory `project_tool_router_prd`).
45
- **Version:** 1.108.113 — **New tool `get_architecture_metrics` (F-15 #4 from the tokensave review, clean-room; competitor unnamed in shipped artifacts). tokensave F-15 series COMPLETE.** Structural concentration + dependency depth + modularity in ONE read-only tool (vs their 3 separate gini/dependency_depth/dsm tools — tool-surface discipline). Over the file import graph (`_build_adjacency` reused from get_dependency_graph): **concentration** = `_gini()` coefficient (0 even→1 hoarded, standard sorted-rank formula) over per-file symbol-count/bytes/fan_in/fan_out + top concentrators (answers "is coupling HOARDED?" which a hotspot peak-list can't); **depth** = Lakos levelization (longest dependency chain + level histogram) over the SCC-condensed DAG via Kahn topo, `_find_cycles` reused for condensation; **modularity** = WCC cluster count + `back_edge_count` (cycle-participating import edges = the hidden coupling a DSM highlights) — NO N×N matrix (that's the bloat we avoid; layering-violation detail stays in get_layer_violations, cycles in get_dependency_cycles — deliberately NOT duplicated). **Does NOT touch the health-radar composite** (observatory comparability preserved — the reason it's a separate tool not new radar axes). Degrades honestly to symbols/bytes Gini when no import data. Read-only analytics, no savings _meta. New `tools/get_architecture_metrics.py`; Architecture group, standard tier (**core_compact unchanged 3969**), tool count **90** full, no CLI, no INDEX_VERSION bump. New `tests/test_architecture_metrics.py` (10). **All 4 tokensave F-15s now done: #1 token-economy=done-by-existing, #2 parity map v1.108.111, #3 decorator census v1.108.112, #4 architecture metrics v1.108.113.**
56
- **Version:** 1.108.112 — **New tool `get_decorator_census` (F-15 #3 from the tokensave review, clean-room; competitor unnamed in shipped artifacts).** Repo-wide census of decorators/annotations/attributes in one read-only call ("where is every `@app.route`/`@Injectable`/`@pytest.fixture`/`[Serializable]`, and how many?"). **Cross-language by construction** — aggregates the `decorators` list the index already stores per symbol (the same field `search_symbols(decorator=)` filters on), so Python decorators + TS/Java annotations + C# attributes all surface with NO parser work (munch-first: the data already existed). Sharper than a flat histogram: forms **normalized** (`_normalize_decorator` strips leading `@`, call-args, `[]` brackets) so `@app.route('/a')`+`@app.route('/b')` collapse to one `app.route` bucket; each bucket keeps distinct `raw_forms` (`_short_raw` flattens+caps at 100 chars so a big `@parametrize([...])` table doesn't dump), `symbol_kinds` breakdown, distinct-file count. Filters: `name_filter` (substring on normalized name), `scope_path` (subtree), `kind`; `include_sites` lists decorated symbols (capped `max_sites_per`). Pairs with get_signal_chains/get_endpoint_impact (surfaces the decorator surface; those resolve what it wires). **Read-only ANALYTICS tool — deliberately NO tokens-saved `_meta`** (no honest full-file-read baseline; matches get_delivery_metrics/get_hotspots — the token-economy honesty finding applied). New `tools/get_decorator_census.py`; standard tier (**core_compact unchanged 3969**), placed in the Architecture group across all surfaces, `schema_baseline.json` refreshed, tool count **89** full; NO CLI (agent-facing MCP query, not a workflow verb); NO INDEX_VERSION bump. New `tests/test_decorator_census.py` (12).
67
- **Version:** 1.108.111 — **New tool `get_parity_map` (migration parity map; F-15 #2 from the tokensave review, clean-room per [[feedback_clean_room_innovation]] — competitor unnamed in shipped artifacts).** Correspondence-aware port parity between a SOURCE and TARGET symbol tree (two subpaths of one repo, or two repos). Per source function/method/class: **ported** (equivalent counterpart), **ported_diverged** (counterpart exists but signature/body drifted — the failure a name-only "exists in both trees" check reports as done), **unported** (no counterpart, still referenced by source), **orphaned** (unmatched with no in-scope caller — entry point or possible drop), **added** (target-only). **Rename-aware:** unmatched source symbols matched to same-kind target candidates by a structural+behavioral blend (`_signature_tokens`/`_callee_set`/`_jaccard`/`_byte_ratio` reused from `find_similar_symbols`), so a ported-and-renamed symbol isn't a false unported+added pair. **Port plan** (`include_port_plan`): dependency-ordered over ALL unmatched source (adjacency from `_callee_set`), Kosaraju SCC grouping via `get_dependency_cycles._find_cycles` (cycles → one `scc_group`, ported together), Kahn topo over the condensed graph → `order_index`, per-symbol `unblocked`/`blocking_deps`. **Read-only / plan-only** (never edits or ports — [[feedback_jcm_read_only_charter]]); `parity_pct = ported/(ported+ported_diverged+unported+orphaned)`, labelled estimate, `added` excluded. Divergence policy `signature` (default, compares param_count + name-stripped signature tokens so a pure rename isn't flagged) / `signature+body` (also content_hash) / `name_only`. New `tools/get_parity_map.py` + `cli/parity.py` + `parity` CLI subcommand; standard tier (**core_compact unchanged 3969**), registered on all surfaces, `schema_baseline.json` refreshed, tool count **88** full. `parity_axes` reserved slot for P3 suite axes (jdoc doc-parity / jdata schema-parity, [[feedback_suite_parity_for_agent_contracts]]). NO INDEX_VERSION bump (pure read). New `tests/test_parity_map.py` (17). Design note: initial "orphaned = no *ported* caller" starved the port plan (swallowed the pending graph); corrected to "no *in-scope* caller" with the plan spanning all unmatched. See `docs/prd-parity-map.md`.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.113"
3+
version = "1.108.114"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/counter.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,38 @@ def search_catalog(
222222
# (compiled_pattern, action, why). Kept deterministic and legible -- this is a
223223
# curated map, not a learned model, consistent with the read-only charter.
224224
_INTENT_RULES: list[tuple[re.Pattern, str, str]] = [
225+
# --- stateful: session / recent-change intents --------------------------- #
226+
# Read the session journal / working-tree delta, not the whole index.
227+
# Placed FIRST: stateful phrasings ("affected by my recent changes") carry
228+
# trigger words for the impact/reference rules below, so they must win first.
229+
(re.compile(r"\b(uncommitted|working tree|staged changes|since (the )?last commit|"
230+
r"changed (since|today|in the last)|what changed|different from main|"
231+
r"diff of what|what i modified|recently modified|"
232+
r"my (last|recent) (edit|change|changes)|last thing i edited|"
233+
r"renamed just now|(edits?|changes?) (are )?pending|pending (edits?|changes?)|"
234+
r"did i (just )?(change|edit)|just changed?)\b", re.I),
235+
"get_changed_symbols", "List symbols changed since the last commit or in the working tree."),
236+
(re.compile(r"\b(this session|we (touched|made|worked|work on)|left off|pick up where|"
237+
r"recap|so far|a minute ago|i was editing|(in the )?last hour)\b", re.I),
238+
"get_session_context", "Recap what this session has touched so far."),
239+
240+
# --- mutate: edit/execute intents ---------------------------------------- #
241+
# jcm is read-only by charter and performs NO edit. When the task is a
242+
# COMMAND to change code, route recognizes it and recommends the read-only
243+
# PREP tool for that edit kind; the agent then applies the change with its
244+
# own editor. Anchored to a LEADING imperative verb so a question about an
245+
# edit ("what breaks if I rename X") falls through to the impact rules and is
246+
# NOT captured here. There is no auto-execute for these (no _QUERY_ARG entry):
247+
# jcm recommends the preflight, it never presumes to run a mutation flow.
248+
(re.compile(r"^\s*rename\b", re.I),
249+
"check_rename_safe", "jcm is read-only; verify a rename is safe, then apply it with your editor."),
250+
(re.compile(r"^\s*(delete|remove)\b", re.I),
251+
"check_delete_safe", "jcm is read-only; check what breaks before you delete, then remove it with your editor."),
252+
(re.compile(r"^\s*(refactor|extract|move|inline)\b", re.I),
253+
"plan_refactoring", "jcm is read-only; get an edit-ready refactor plan, then apply it with your editor."),
254+
(re.compile(r"^\s*(add|write|create|implement|fix|update|convert|reformat|change|wrap|generate|apply)\b", re.I),
255+
"check_edit_safe", "jcm is read-only; preflight the edit's risk, then modify with your editor."),
256+
225257
(re.compile(r"\b(who )?calls?\b|\bcallers?\b|\bcall(ed)? by\b|\bcall (graph|hierarchy)\b", re.I),
226258
"get_call_hierarchy", "Trace callers/callees of a symbol."),
227259
(re.compile(r"\bused? (by|where)\b|\breferences?\b|\bwhere is .* used\b", re.I),

tests/test_v1_108_114.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""v1.108.114 — route() gains mutate + stateful intent buckets.
2+
3+
The leakage finding: route() had no bucket for edit/execute commands or for
4+
session/recent-change queries, so those intents fell through to search_symbols
5+
or NO_MATCH. This adds both, on-charter: mutate is recognize-and-redirect to a
6+
READ-ONLY prep tool (jcm never performs the edit), stateful maps to the
7+
session/delta tools.
8+
"""
9+
import re
10+
11+
from jcodemunch_mcp import counter
12+
13+
# A catalog superset covering the new bucket targets + the pre-existing ones.
14+
NAMES = {
15+
"search_symbols", "search_text", "get_file_outline", "get_repo_health",
16+
"get_call_hierarchy", "find_references", "get_blast_radius",
17+
"get_class_hierarchy", "get_dependency_graph", "find_dead_code",
18+
"assemble_task_context",
19+
# new bucket targets
20+
"get_changed_symbols", "get_session_context",
21+
"check_rename_safe", "check_delete_safe", "plan_refactoring", "check_edit_safe",
22+
}
23+
24+
MUTATE_PREP_TOOLS = {"check_rename_safe", "check_delete_safe",
25+
"plan_refactoring", "check_edit_safe"}
26+
STATEFUL_TOOLS = {"get_changed_symbols", "get_session_context"}
27+
28+
29+
def _top(task):
30+
recs = counter.classify_intent(task, NAMES)
31+
return recs[0]["action"] if recs else None
32+
33+
34+
# --- mutate bucket --------------------------------------------------------- #
35+
36+
def test_mutate_commands_route_to_readonly_prep():
37+
cases = {
38+
"rename UserRepository to AccountRepository everywhere": "check_rename_safe",
39+
"delete the unused helper functions": "check_delete_safe",
40+
"remove the dead config loader": "check_delete_safe",
41+
"refactor the auth module": "plan_refactoring",
42+
"extract this into a separate method": "plan_refactoring",
43+
"move the config loading into its own module": "plan_refactoring",
44+
"inline the temporary variable": "plan_refactoring",
45+
"add a retry to the http client": "check_edit_safe",
46+
"fix the bug in parse_config": "check_edit_safe",
47+
"implement pagination for the results endpoint": "check_edit_safe",
48+
"update the docstring on createSession": "check_edit_safe",
49+
"change the timeout to 60 seconds": "check_edit_safe",
50+
"write a function that validates emails": "check_edit_safe",
51+
"generate a migration for the new column": "check_edit_safe",
52+
"apply the fix and run the tests": "check_edit_safe",
53+
}
54+
for task, expected in cases.items():
55+
assert _top(task) == expected, f"{task!r} -> {_top(task)} (want {expected})"
56+
57+
58+
def test_mutate_recommendations_are_readonly_charter_safe():
59+
"""The mutate bucket must only ever recommend read-only prep tools —
60+
never a state-changing or exec/write action. This is the charter guard."""
61+
for tool in MUTATE_PREP_TOOLS:
62+
assert not counter.is_state_changing(tool), f"{tool} is state-changing"
63+
assert counter.forbidden_reason(tool) is None, f"{tool} trips the exec/write tripwire"
64+
65+
66+
def test_mutate_why_declares_readonly():
67+
recs = counter.classify_intent("rename Foo to Bar", NAMES)
68+
assert recs and "read-only" in recs[0]["why"].lower()
69+
70+
71+
def test_edit_question_is_not_captured_as_mutate():
72+
"""Leading-verb anchor: a QUESTION about an edit must fall through to the
73+
impact rules, not the mutate bucket (which is for imperative commands)."""
74+
assert _top("what would be affected if I rename UserRepository") not in MUTATE_PREP_TOOLS
75+
assert _top("what breaks if I change the signature of createSession") not in MUTATE_PREP_TOOLS
76+
77+
78+
# --- stateful bucket ------------------------------------------------------- #
79+
80+
def test_stateful_change_queries_route_to_delta_tool():
81+
for task in [
82+
"what did I just change",
83+
"show me my uncommitted edits",
84+
"what symbols changed since the last commit",
85+
"what's different from main",
86+
"which functions did my last edit affect",
87+
"list files changed today",
88+
"what's in my working tree that's new",
89+
"show recently modified symbols",
90+
"the classes I renamed just now",
91+
"what edits are pending",
92+
]:
93+
assert _top(task) in STATEFUL_TOOLS, f"{task!r} -> {_top(task)}"
94+
95+
96+
def test_stateful_session_queries_route_to_session_tool():
97+
for task in [
98+
"what have we touched in this session",
99+
"pick up where we left off",
100+
"recap the changes from this branch",
101+
"what did we work on earlier",
102+
"the file I was editing a minute ago",
103+
]:
104+
assert _top(task) in STATEFUL_TOOLS, f"{task!r} -> {_top(task)}"
105+
106+
107+
def test_stateful_does_not_steal_semantic_session_word():
108+
"""'where does session state get persisted' is a semantic code query, not a
109+
stateful session recap — the bare word 'session' must not trigger the bucket."""
110+
assert _top("where does session state get persisted") not in STATEFUL_TOOLS
111+
112+
113+
# --- no regression to existing routes -------------------------------------- #
114+
115+
def test_existing_routes_unchanged():
116+
assert _top("who calls this function") == "get_call_hierarchy"
117+
assert _top("find dead code in the project") == "find_dead_code"
118+
assert _top("search for the string TODO in the code") == "search_text"
119+
assert _top("find the UserRepository class") == "search_symbols"
120+
121+
122+
def test_mutate_prep_not_auto_executed():
123+
"""Mutate prep tools have no _QUERY_ARG entry -> route recommends, never
124+
auto-executes a mutation flow from a free-form task."""
125+
for tool in MUTATE_PREP_TOOLS:
126+
assert counter.shape_execute_args(tool, "some/repo", "rename Foo to Bar") is None

0 commit comments

Comments
 (0)