Skip to content

Commit b652df3

Browse files
HumanBean17claude
andauthored
chore: polish bundle — correctness/robustness nits (#359) (#370)
* chore: polish bundle — small correctness/robustness nits (#359) - exclude_roles now disables role weighting (search_lancedb): skip_role_weight was bool(role or role_in), so an exclude_roles-only filter still applied role weights — asymmetric with role/role_in. Include exclude_roles. - _parse_ladybug_json quotes only key positions (ladybug_queries): the old (\\w+): regex matched word-colon runs inside values too (e.g. a URL), corrupting them. Quote keys only after { , [ ; fix the misleading "counts_json" log label. - resolve rejects wildcards consistently (mcp_v2): * / ? silently returned status='none'; search/find/neighbors reject them. Now success=False with a pointer to search(query=...). Updated the wildcard test accordingly. - client-kind literals de-leaked: feign_method/rest_template/web_client emitted and matched directly; reference named CLIENT_KIND_* constants from java_ontology so a rename can't desync the emit/match sites. - CALLS dedup key includes call_site_byte (build_ast_graph): two call sites of the same method on the same source line (same arg_count) no longer collapse into one edge. Regenerated the bank-chat baseline CALLS count (678 -> 684) and mirrored the byte-inclusive key in the dedup test. - malformed YAML no longer silently swallowed (config): catch yaml.YAMLError and emit a stderr hint instead of a bare except returning {}. - table_names() -> list_tables() (config, cli): the lancedb deprecation warning is gone (lance_optimize already preferred list_tables). Deferred: the FastMCP run_stdio_async "coroutine never awaited" RuntimeWarning — benign (the server runs; run_stdio_async is a clean coroutine awaited by asyncio.run), root cause elusive without FastMCP stdio-transport internals, and a speculative entry-point change risks destabilizing server startup. Co-Authored-By: Claude <noreply@anthropic.com> * fix(graph): count CALLS with the same 5-tuple key used to write them _write_meta deduped CALLS with the pre-#370 4-tuple (no call_site_byte) while _write_edges wrote edges with the 5-tuple, so counts['calls'] (678) diverged from the real CALLS edge count (684) and describe/stats undercounted. Mirror the 5-tuple in _write_meta and bump the bank-chat baseline's counts_json.calls to 684. Verified red->green: with only the fixture bumped, the baseline test fails (rebuild yields 678); with the _write_meta fix it passes. Co-Authored-By: Claude <noreply@anthropic.com> * fix(enrich): use CLIENT_KIND_REST_TEMPLATE at the synthesized-hint site The client-kind literal de-leak missed graph_enrich.py, which still emitted the bare "rest_template" default when synthesizing a brownfield HttpClientHint with no anchor. Reference the named constant so a future rename in VALID_CLIENT_KINDS cannot silently desync this emit site. Co-Authored-By: Claude <noreply@anthropic.com> * fix(config): keep load_yaml_mapping best-effort for read/decode errors Narrowing the except to yaml.YAMLError let OSError (chmod 000, stat/read TOCTOU) and UnicodeDecodeError (non-UTF-8 config) propagate and abort startup, breaking the loader's return-{} contract. Catch all three so an unreadable/malformed config degrades to defaults; keep the stderr hint (generalized). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f902fb7 commit b652df3

13 files changed

Lines changed: 112 additions & 33 deletions

ast_java.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,6 +1868,11 @@ def _collect_outgoing_calls(
18681868
file_rel: str,
18691869
) -> list[OutgoingCallDecl]:
18701870
del project_root
1871+
from java_ontology import ( # deferred: java_ontology imports ast_java
1872+
CLIENT_KIND_FEIGN_METHOD,
1873+
CLIENT_KIND_REST_TEMPLATE,
1874+
CLIENT_KIND_WEB_CLIENT,
1875+
)
18711876
out: list[OutgoingCallDecl] = []
18721877
method_fqn = f"{type_fqn}#{method_decl.signature}"
18731878
type_mods = _find_modifiers_child(type_node) if type_node is not None else None
@@ -1899,7 +1904,7 @@ def _collect_outgoing_calls(
18991904
OutgoingCallDecl(
19001905
method_fqn=method_fqn,
19011906
method_sig=method_decl.signature,
1902-
client_kind="feign_method",
1907+
client_kind=CLIENT_KIND_FEIGN_METHOD,
19031908
channel="http",
19041909
feign_target_name=feign_target_name,
19051910
feign_target_url=feign_target_url,
@@ -1998,7 +2003,7 @@ def visit(n: Node) -> None:
19982003
OutgoingCallDecl(
19992004
method_fqn=method_fqn,
20002005
method_sig=method_decl.signature,
2001-
client_kind="rest_template",
2006+
client_kind=CLIENT_KIND_REST_TEMPLATE,
20022007
channel="http",
20032008
feign_target_name="",
20042009
feign_target_url="",
@@ -2051,7 +2056,7 @@ def visit(n: Node) -> None:
20512056
OutgoingCallDecl(
20522057
method_fqn=method_fqn,
20532058
method_sig=method_decl.signature,
2054-
client_kind="web_client",
2059+
client_kind=CLIENT_KIND_WEB_CLIENT,
20552060
channel="http",
20562061
feign_target_name="",
20572062
feign_target_url="",

build_ast_graph.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,13 @@
6666
symbol_id,
6767
)
6868
from path_filtering import LayeredIgnore, iter_java_source_files
69-
from java_ontology import VALID_CLIENT_KINDS, VALID_HTTP_CALL_MATCHES, VALID_PRODUCER_KINDS
69+
from java_ontology import (
70+
CLIENT_KIND_FEIGN_METHOD,
71+
CLIENT_KIND_REST_TEMPLATE,
72+
VALID_CLIENT_KINDS,
73+
VALID_HTTP_CALL_MATCHES,
74+
VALID_PRODUCER_KINDS,
75+
)
7076

7177
log = logging.getLogger(__name__)
7278

@@ -2403,7 +2409,7 @@ def _phantom_async_route_id(call: OutgoingCallDecl) -> str:
24032409
)
24042410
rid = ""
24052411
strategy = call.resolution_strategy
2406-
if call.client_kind == "feign_method":
2412+
if call.client_kind == CLIENT_KIND_FEIGN_METHOD:
24072413
exposing = next((e for e in tables.exposes_rows if e.symbol_id == member.node_id), None)
24082414
if exposing is not None:
24092415
rid = exposing.route_id
@@ -2606,7 +2612,7 @@ def _match_call_edge(
26062612
return "unresolved", []
26072613

26082614
candidates: list[RouteRow] = []
2609-
if call.client_kind == "feign_method":
2615+
if call.client_kind == CLIENT_KIND_FEIGN_METHOD:
26102616
# Prefer endpoint matching by target service + path/method for Feign declarations.
26112617
path_value = call.path_template_call
26122618
method_value = call.method_call
@@ -2735,7 +2741,7 @@ def _micro_factor(member: MemberEntry | None) -> float:
27352741
if src_route is None and member is not None:
27362742
# Recover feign caller hints from persisted caller-side Client declarations.
27372743
for client in client_hints_by_member.get(member.node_id, ()):
2738-
if client.client_kind != "feign_method":
2744+
if client.client_kind != CLIENT_KIND_FEIGN_METHOD:
27392745
continue
27402746
path_template, path_regex = _normalize_path(client.path)
27412747
src_route = RouteRow(
@@ -2771,7 +2777,7 @@ def _micro_factor(member: MemberEntry | None) -> float:
27712777
call = OutgoingCallDecl(
27722778
method_fqn=f"{member.parent_fqn}#{member.decl.signature}" if member else "",
27732779
method_sig=member.decl.signature if member else "",
2774-
client_kind="feign_method" if _feign_like else "rest_template",
2780+
client_kind=CLIENT_KIND_FEIGN_METHOD if _feign_like else CLIENT_KIND_REST_TEMPLATE,
27752781
channel="http",
27762782
feign_target_name=src_route.feign_name if src_route else "",
27772783
feign_target_url=src_route.feign_url if src_route else "",
@@ -3454,13 +3460,15 @@ def _write_edges(conn: ladybug.Connection, tables: GraphTables, _file_by_node_id
34543460
_bulk_copy(conn, "OVERRIDES", _REL_OVERRIDES_COLUMNS, overrides_rows)
34553461

34563462
# Stage CALLS rows with dedup and callee_declaring_role materialization
3457-
seen_calls: set[tuple[str, str, int, int]] = set()
3463+
seen_calls: set[tuple[str, str, int, int, int]] = set()
34583464
calls_rows: list[dict] = []
34593465
member_by_id = {m.node_id: m for m in tables.members}
34603466
for row in tables.calls_rows:
34613467
if row.src_id not in valid_ids or row.dst_id not in valid_ids:
34623468
continue
3463-
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line)
3469+
# Include call_site_byte so two call sites of the same method on the same
3470+
# source line (same arg_count) are kept as distinct edges (issue #359).
3471+
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line, row.call_site_byte)
34643472
if key in seen_calls:
34653473
continue
34663474
seen_calls.add(key)
@@ -3636,10 +3644,15 @@ def _write_routes_and_exposes(conn: ladybug.Connection, tables: GraphTables, _fi
36363644

36373645

36383646
def _write_meta(conn: ladybug.Connection, tables: GraphTables, source_root: Path) -> None:
3639-
seen_calls: set[tuple[str, str, int, int]] = set()
3647+
# Dedup key MUST match _write_edges (build_ast_graph.py, _REL_CALLS writer): the
3648+
# 5-tuple includes call_site_byte so two call sites of the same method on the
3649+
# same source line are counted separately. A previous version used the 4-tuple
3650+
# here, which made counts['calls'] (678) diverge from the real CALLS edge count
3651+
# (684) that _write_edges actually persisted — describe/stats then undercounted.
3652+
seen_calls: set[tuple[str, str, int, int, int]] = set()
36403653
calls_unique = 0
36413654
for row in tables.calls_rows:
3642-
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line)
3655+
key = (row.src_id, row.dst_id, row.arg_count, row.call_site_line, row.call_site_byte)
36433656
if key not in seen_calls:
36443657
seen_calls.add(key)
36453658
calls_unique += 1

graph_enrich.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
_TYPE_ANN_TO_CAPABILITY,
4444
)
4545
from java_ontology import (
46+
CLIENT_KIND_REST_TEMPLATE,
4647
VALID_CAPABILITIES,
4748
VALID_CLIENT_KINDS,
4849
VALID_PRODUCER_KINDS,
@@ -1301,7 +1302,7 @@ def resolve_http_client_for_method(
13011302
hint = overrides.annotation_to_http_client_hint.get("CodebaseHttpClient")
13021303
if hint is None:
13031304
hint = HttpClientHint(
1304-
client_kind=anchor.client_kind if anchor else "rest_template",
1305+
client_kind=anchor.client_kind if anchor else CLIENT_KIND_REST_TEMPLATE,
13051306
target_service=anchor.feign_target_name if anchor else "",
13061307
path=anchor.path_template_call if anchor else "",
13071308
method=anchor.method_call if anchor else "",

java_codebase_rag/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ def _cmd_erase(args: argparse.Namespace) -> int:
617617
import lancedb
618618

619619
db = lancedb.connect(str(cfg.index_dir.resolve()))
620-
for name in db.table_names():
620+
for name in db.list_tables():
621621
to_describe.append(cfg.index_dir / name)
622622
except Exception:
623623
pass
@@ -665,7 +665,7 @@ def work(progress: "PipelineProgress | None") -> int:
665665
import lancedb
666666

667667
db = lancedb.connect(str(cfg.index_dir.resolve()))
668-
for name in list(db.table_names()):
668+
for name in list(db.list_tables()):
669669
try:
670670
db.drop_table(name)
671671
except Exception as exc:

java_codebase_rag/config.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,17 @@ def load_yaml_mapping(source_root: Path) -> dict[str, Any]:
244244
return {}
245245
try:
246246
data = yaml.safe_load(path.read_text(encoding="utf-8"))
247-
except Exception:
247+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as exc:
248+
# Best-effort loader: a missing/unreadable/malformed config must NOT abort
249+
# startup — return {} and proceed with defaults. Narrowing this to
250+
# ``yaml.YAMLError`` alone let OSError (chmod 000, stat/read TOCTOU) and
251+
# UnicodeDecodeError (non-UTF-8 config) propagate to the caller; the broader
252+
# tuple restores the graceful-degradation contract while still surfacing the
253+
# problem on stderr.
254+
print(
255+
f"java-codebase-rag: could not load config {path}: {exc}; ignoring config.",
256+
file=sys.stderr,
257+
)
248258
return {}
249259
return data if isinstance(data, dict) else {}
250260

@@ -483,7 +493,7 @@ def index_dir_has_existing_artifacts(index_dir: Path) -> tuple[bool, list[str]]:
483493
import lancedb
484494

485495
db = lancedb.connect(str(index_dir.resolve()))
486-
for name in db.table_names():
496+
for name in db.list_tables():
487497
paths.append(str((index_dir / name).resolve()) + " (Lance table)")
488498
except Exception:
489499
pass

java_ontology.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"rest_template",
5050
"web_client",
5151
))
52+
# Named members of VALID_CLIENT_KINDS — reference these at emit/match sites so a
53+
# rename in the set above cannot silently desync callers (issue #359).
54+
CLIENT_KIND_FEIGN_METHOD = "feign_method"
55+
CLIENT_KIND_REST_TEMPLATE = "rest_template"
56+
CLIENT_KIND_WEB_CLIENT = "web_client"
5257

5358
VALID_PRODUCER_KINDS: frozenset[str] = frozenset((
5459
"kafka_send",
@@ -433,6 +438,9 @@ class EdgeSpec:
433438
"VALID_ROUTE_FRAMEWORKS",
434439
"VALID_ROUTE_KINDS",
435440
"VALID_CLIENT_KINDS",
441+
"CLIENT_KIND_FEIGN_METHOD",
442+
"CLIENT_KIND_REST_TEMPLATE",
443+
"CLIENT_KIND_WEB_CLIENT",
436444
"VALID_PRODUCER_KINDS",
437445
"VALID_HTTP_CALL_STRATEGIES",
438446
"VALID_ASYNC_CALL_STRATEGIES",

ladybug_queries.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,27 @@
3030

3131

3232
def _parse_ladybug_json(raw: str | None) -> dict[str, Any]:
33-
"""Parse JSON from LadybugDB which returns unquoted keys like {key: value}."""
33+
"""Parse JSON from LadybugDB which returns unquoted keys like {key: value}.
34+
35+
Only quote keys at key positions (after ``{``, ``,`` or ``[``) so values
36+
containing word-colon patterns (e.g. a URL ``https://...`` inside a quoted
37+
string) are not corrupted. The previous ``(\\w+):`` regex matched ``word:``
38+
anywhere, including inside values (issue #359).
39+
"""
3440
if not raw:
3541
return {}
36-
# LadybugDB returns JSON without quotes around keys: {packages: 1, files: 2}
37-
# Convert to standard JSON: {"packages": 1, "files": 2}
38-
# This regex matches word characters followed by ':' at the start of a key
39-
quoted = re.sub(r'(\w+):', r'"\1":', raw)
42+
# Quote unquoted keys only where a key is expected: preceded by '{', ',' or
43+
# '[' (with optional whitespace). This leaves word-colon runs inside values
44+
# untouched.
45+
quoted = re.sub(r'([,{\[]\s*)(\w+):', lambda m: f'{m.group(1)}"{m.group(2)}":', raw)
4046
try:
4147
return json.loads(quoted)
4248
except Exception:
4349
try:
44-
# Fallback: try parsing as-is (for standard JSON)
50+
# Fallback: try parsing as-is (for standard JSON).
4551
return json.loads(raw)
4652
except Exception:
47-
log.warning("Failed to parse counts_json: %s", raw[:100])
53+
log.warning("Failed to parse graph_meta JSON blob: %s", raw[:100])
4854
return {}
4955

5056
# Composed describe / neighbors dot-keys (not stored graph edge labels).

mcp_v2.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1533,7 +1533,18 @@ def resolve_v2(
15331533

15341534
assert trimmed is not None
15351535
if "*" in trimmed or "?" in trimmed:
1536-
return _resolve_finalize_success(trimmed, hint_kind, [])
1536+
out = ResolveOutput(
1537+
success=False,
1538+
status="none",
1539+
message=(
1540+
"Wildcards (* and ?) are not supported in resolve; "
1541+
"use search(query=...) for ranked text search."
1542+
),
1543+
advisories=[],
1544+
resolved_identifier=trimmed,
1545+
)
1546+
_resolve_assert_invariants(out)
1547+
return out
15371548

15381549
g = graph or LadybugGraph.get()
15391550
raw: list[tuple[NodeRef, ResolveReason, int]] = []

search_lancedb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ def run_search(
853853
capability=capability, capability_in=capability_in,
854854
) if "java" in table_keys else []
855855

856-
skip_role_weight = bool(role or role_in)
856+
skip_role_weight = bool(role or role_in or exclude_roles)
857857
query_toks = _query_tokens(query)
858858

859859
if len(table_keys) == 1:

tests/fixtures/graph_baseline_bank_chat.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
"INJECTS": 94,
77
"DECLARES": 606,
88
"OVERRIDES": 38,
9-
"CALLS": 678,
9+
"CALLS": 684,
1010
"UNRESOLVED_AT": 227
1111
},
1212
"graph_meta": {
1313
"ontology_version": 17,
1414
"built_at": 1782110216,
1515
"source_root": "/Users/dmitry/Desktop/CursorProjects/java-enterprise-codebase-rag/tests/bank-chat-system",
16-
"counts_json": "{packages: 29, files: 130, types: 140, members: 606, phantoms: 54, extends: 18, implements: 21, injects: 94, declares: 606, overrides: 38, calls: 678, routes: 29, exposes: 15, clients: 8, declares_client: 8, producers: 9, declares_producer: 9, http_calls: 8, async_calls: 9}"
16+
"counts_json": "{packages: 29, files: 130, types: 140, members: 606, phantoms: 54, extends: 18, implements: 21, injects: 94, declares: 606, overrides: 38, calls: 684, routes: 29, exposes: 15, clients: 8, declares_client: 8, producers: 9, declares_producer: 9, http_calls: 8, async_calls: 9}"
1717
},
1818
"sampled_edges": {
1919
"EXTENDS": [

0 commit comments

Comments
 (0)