Skip to content

Commit 7d0e996

Browse files
jgravelleclaude
andcommitted
fix: composite preflights consume reference evidence; cross-repo batch fails closed (#338, #339)
#338 — check_edit_safe / check_delete_safe called check_references in singular mode (identifier=, flat response) but iterated the batch-only `results` key, so identifier content-references were always counted as zero and a still-referenced symbol could come back safe_to_delete. Both call sites now use identifiers=[...]. #339 — find_importers(file_paths=[...], cross_repo=true) passed an empty path into the package-level cross-repo helper for multi-file batches. The combination now fails closed with a clear error; single-element batches stay honored. Bump to 1.108.63. New tests/test_v1_108_63.py (7 cases). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5e61a17 commit 7d0e996

9 files changed

Lines changed: 215 additions & 6 deletions

File tree

CHANGELOG.md

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

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

5+
## [1.108.63] - 2026-06-19 - Composite preflights consume reference evidence; cross-repo batch fails closed
6+
7+
### Fixed
8+
9+
- **`check_edit_safe` and `check_delete_safe` no longer drop identifier
10+
reference evidence** (issue #338, reported by @mmashwani). Both composite
11+
preflights called `check_references` in singular mode (`identifier=...`), which
12+
returns a flat response with top-level `import_references` / `content_references`
13+
and no `results` key, but then iterated the batch-only `results` array. The
14+
identifier content-reference signal was therefore always counted as zero, so a
15+
symbol still referenced by a duck-typed/string-dispatched call site (no import
16+
edge) could be reported `safe_to_delete`. Both call sites now use the batch form
17+
(`identifiers=[target_name]`), which produces the grouped `results` shape the
18+
loops already expect — the external preflight response shape is unchanged.
19+
- **`find_importers(file_paths=[...], cross_repo=true)` no longer silently drops
20+
cross-repo evidence on multi-file batches** (issue #339, reported by
21+
@mmashwani). The batch branch passed an empty file path into the cross-repo
22+
helper for batches of more than one file. Because `_find_cross_repo_importers`
23+
is package-level (it resolves the *repo's* package importers, not a per-file
24+
result), attaching one whole-repo result across a batch would either drop the
25+
evidence or imply a per-file precision that does not exist. The combination now
26+
**fails closed** with a clear error directing callers to singular `file_path`
27+
calls for cross-repo evidence; a single-element `file_paths` batch with
28+
`cross_repo=true` is still honored (equivalent to the singular path).
29+
- New regression tests `tests/test_v1_108_63.py` (7 cases): content-reference
30+
blocking on delete/edit preflights with a no-false-positive orphan guard, and
31+
the cross-repo batch contract (multi-file fails closed, single-file allowed,
32+
no-cross-repo batch and singular paths unaffected).
33+
534
## [1.108.62] - 2026-06-19 - Index templating-language files over a supported underlying language
635

736
### 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.63 — Two composite/contract correctness fixes (both reported by @mmashwani, source-cited). **#338**: `check_edit_safe` + `check_delete_safe` called `check_references(identifier=...)` (singular → flat `import_references`/`content_references`, no `results` key) then iterated the batch-only `ref_out.get("results", [])`, so the identifier content-reference signal (Signal 2/3) was ALWAYS zero — a symbol still referenced by a duck-typed/string-dispatch call site (no import edge, so `find_importers`/Signal 1 misses it too) could return `safe_to_delete`. Fix = both call sites switch to the batch form `identifiers=[target_name]`, producing the grouped `results` shape the loops already expect (external preflight response shape unchanged). **#339**: `find_importers(file_paths=[...], cross_repo=true)` passed `""` into `_find_cross_repo_importers` for multi-file batches. That helper is **package-level** (it resolves the repo's package importers and ignores its `file_path` arg entirely), so a multi-file batch either dropped the evidence or implied per-file precision that doesn't exist; now **fails closed** with a clear error (`_meta.cross_repo_scope=package`, `file_count`) pointing at singular calls, while a single-element `file_paths` batch is still honored (== singular path). Docstring + `server.py` schema for `find_importers.cross_repo` document the constraint. New `tests/test_v1_108_63.py` (7): content-reference blocking on delete/edit + no-false-positive orphan guard + cross-repo batch contract (multi-file errors, single-file allowed, no-cross-repo + singular unaffected). 165 passed across the four affected suites + new file. Shipped to PyPI. See [[project_community_mmashwani]]. **#340 (bounded-source mode for `get_symbol_source`) accepted-in-principle, deferred to a follow-up — enhancement w/ API-surface design choices.**
45
- **Version:** 1.108.62 — Index templating-language files over a supported underlying source language (#336, PR #337 by @horknfbr). Files of the form `name.<underlying-ext>.<engine-ext>` (Jinja2 template of TypeScript `foo.ts.j2`, Jinja of Python `service.py.jinja`, Twig of TS `widget.ts.twig`) were dropped at discovery as `wrong_extension` (`get_language_for_path` resolved the compound from the first dot → `.ts.j2` unregistered → `None`), so every real source symbol inside went unindexed. Now recognized: engine constructs are **masked offset-preserving** (each `{{ }}`/`{% %}`/`{# #}` hole → same-length `_` filler, newlines kept) and the body is re-parsed as its underlying language via recursive `parse_file()`, so symbols/signatures/imports/line+byte positions all resolve with **no block re-wrapping** (simpler than the Astro/Razor sub-block extractors). **Engines:** Jinja2 (`.j2`/`.jinja`/`.jinja2`) + Twig (`.twig`); pluggable `TemplateEngine` registry in new `parser/template_shared.py` (single-extension HTML-bodied engines can be added on demand). Underlying language inferred from the **middle extension** by stripping the engine suffix and resolving the remainder recursively — so every supported language works as the body with no per-language work. **Discovery guard:** the strip/recurse step in `get_language_for_path` sits *after* the compound + last-extension checks and fires only for registered engine extensions, so it can only ever turn `None` into a language, never re-resolve a `.d.ts`/`.test.ts`/`.blade.php` (non-collision tests included). `{% macro %}`/`{% block %}` definitions additionally surfaced as symbols, **reusing** `sql_preprocessor.extract_dbt_directives` (generalized to take a `directive_keywords` set; the default dbt set stays byte-identical) rather than a parallel regex. `repo` forwarded into the recursive body parse so the underlying language honors per-project `.jcodemunch.jsonc` gating. **Purely additive — previously-skipped files only add symbols, no `INDEX_VERSION` bump** (a re-index picks up the now-recognized files). New `parser/template_shared.py`; `extractor.py` `_parse_template_symbols`; `imports.py` template branch; `languages.py` `template_underlying_language` + `_TEMPLATE_LANG_SPEC`. New `tests/test_templates.py` (17 cases) + fixtures; `LANGUAGE_SUPPORT.md` updated. Verified pre-merge against the PR branch: 17/17 template tests + 607-case parser/sql/dbt/astro/razor/languages/importers sweep all green, dbt path byte-identical. Shipped to PyPI (first contributor template-language feature). See [[project_jcm_336_template_indexing]].
56
- **Version:** 1.108.61 — Config-template completeness. 13 keys lived in `DEFAULTS`/`CONFIG_TYPES` but were never added to the JSONC template that `config_report` parses for `group`+`description`, so `config --json` emitted them under the `Other` catch-all with an empty description (surfaced via the jMunch Console's grouped config screen showing blank rows). Added all 13 to `generate_template()` under proper `=== Section ===` headers + inline comments: `watch_paths`/`watch_extra_ignore`/`watch_follow_symlinks`/`watch_idle_timeout`/`watch_log` → **Watcher**; `runtime_ingest_enabled`/`org_ingest_enabled`/`runtime_ingest_max_body_bytes` → new **Runtime & Org HTTP Ingest**; `license_key` → new **Licensing**; `cross_repo_default`/`discovery_hint` → **Identity & Indexing Behavior**; `agent_selector` → new **Agent Selector (model routing)**; `enrichment` → new **Enrichment (LSP)**. `Other` is now empty. Metadata is parsed from the template (not the user's on-disk config) so descriptions appear immediately on upgrade — no `config --upgrade` needed. New regression test `TestConfigReportGrouping::test_no_key_falls_into_other_or_missing_description` fails if any future `DEFAULTS` key ships template-less. No config *value* behavior change. See [[project_jmunch_console]].
67
- **Version:** 1.108.60 — #335 (reported by @mmashwani). `config --check` treated *every* index-storage probe exception as a confirmed `storage` failure (exit 1), so a sandboxed agent shell that can read the index but can't write the index root (`PermissionError` EPERM/EACCES on the `.jcm_probe` write) reported a healthy install as broken — exactly the false-failure an agent might "repair." Fix (his suggested shape): split the probe's `except` — a `PermissionError` with `errno` ∈ {EPERM, EACCES} is **host-confirmation-needed** (`⚠ index storage writability needs host confirmation` + a "rerun outside a sandbox/restricted shell" hint, recorded in a new `host_confirmation` list), NOT a confirmed failure; any other exception (e.g. `EROFS` read-only FS) stays a confirmed `✗ index storage not writable` issue. Exit contract: confirmed `issues` → exit 1 (unchanged); host-confirmation-only (no confirmed issues) → **exit 0** so an agent client doesn't mistake a healthy install for a broken one (a new nonzero exit code would risk the same false-failure, so deliberately not introduced). `import errno` added to server.py. 4 tests `tests/test_config.py::TestServerConfigCheckStorageProbe` (writable→pass/exit0; EROFS→confirmed/exit1; EPERM→host-confirmation/no-exit; output names sandbox + rerun).

LAST_SESSION.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Last Session
2+
3+
- **Session ID:** 2026-05-12-closer
4+
- **Timestamp:** 2026-05-12
5+
- **Focus:** Phase-2 sibling-parity completion across jData + jDoc + jcm Windows hook fix
6+
7+
## What shipped
8+
- jData v1.10.0: get_redaction_log + get_data_hotspots v2
9+
- jData v1.11.0: data_health_radar + diff_data_health_radar
10+
- jData v1.12.0: find_similar_columns (Phase-2 jData COMPLETE)
11+
- jDoc v1.62.0: doc_health_radar + diff_doc_health_radar
12+
- jDoc v1.63.0: get_doc_pr_risk_profile (Phase-2 jDoc COMPLETE)
13+
- jcm v1.108.7: Windows hook path bug — forward slashes + path-shape-agnostic dedup
14+
15+
## PRD status
16+
PRD_sibling_parity_v1.md closed as shipped. Phase 3 reviewed and not pursued.
17+
18+
## Memory written this session
19+
- project_sibling_parity_phase2.md (updated)
20+
- reference_suite_parity_matrix.md (new)
21+
- feedback_windows_hook_paths.md (new)

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.62"
3+
version = "1.108.63"
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/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1391,7 +1391,7 @@ def _build_tools_list() -> list[Tool]:
13911391
"file_path": {"type": "string", "description": "Target file path within the repo (e.g. 'src/features/intake/IntakeService.js'). Use for single-file queries. Cannot be used together with file_paths."},
13921392
"file_paths": {"type": "array", "items": {"type": "string"}, "description": "List of target file paths for batch queries. Returns a results array. Cannot be used together with file_path."},
13931393
"max_results": {"type": "integer", "default": 50, "description": "Maximum results per file"},
1394-
"cross_repo": {"type": "boolean", "default": False, "description": "When true, also search other indexed repos for cross-repo importers. Default: false (or JCODEMUNCH_CROSS_REPO_DEFAULT env var)."},
1394+
"cross_repo": {"type": "boolean", "default": False, "description": "When true, also search other indexed repos for cross-repo importers (package-level scope). Default: false (or JCODEMUNCH_CROSS_REPO_DEFAULT env var). Only valid with singular file_path or a single-element file_paths batch; combined with a multi-file file_paths batch it returns an error (use singular calls for cross-repo evidence)."},
13951395
},
13961396
"required": ["repo"],
13971397
},

src/jcodemunch_mcp/tools/check_delete_safe.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,11 @@ def check_delete_safe(
249249
test_ref_count = 0
250250
try:
251251
from .check_references import check_references # noqa: PLC0415
252+
# Batch form (identifiers=[...]) so check_references returns its grouped
253+
# `results` shape — singular (identifier=...) returns a flat response with
254+
# no `results` key, which this loop would silently read as empty (#338).
252255
ref_out = check_references(
253-
repo=f"{owner}/{name}", identifier=target_name,
256+
repo=f"{owner}/{name}", identifiers=[target_name],
254257
search_content=True, max_content_results=20,
255258
storage_path=storage_path,
256259
)

src/jcodemunch_mcp/tools/check_edit_safe.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,11 @@ def check_edit_safe(
143143
test_ref_count = 0
144144
try:
145145
from .check_references import check_references # noqa: PLC0415
146+
# Batch form (identifiers=[...]) so check_references returns its grouped
147+
# `results` shape — singular (identifier=...) returns a flat response with
148+
# no `results` key, which this loop would silently read as empty (#338).
146149
ref_out = check_references(
147-
repo=f"{owner}/{name}", identifier=target_name,
150+
repo=f"{owner}/{name}", identifiers=[target_name],
148151
search_content=True, max_content_results=20,
149152
storage_path=storage_path,
150153
)

src/jcodemunch_mcp/tools/find_importers.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ def find_importers(
270270
storage_path: Custom storage path.
271271
cross_repo: When True, also search other indexed repos for cross-repo importers.
272272
Defaults to the ``cross_repo_default`` config value (False).
273+
Package-level (whole-repo) scope, so it is only supported with
274+
singular ``file_path`` or a single-element ``file_paths`` batch;
275+
combining it with a multi-file batch returns an error (#339).
273276
274277
Returns:
275278
Singular mode: dict with flat ``importers`` list and _meta envelope.
@@ -305,14 +308,31 @@ def find_importers(
305308
repo_id = f"{owner}/{name}"
306309

307310
if file_paths is not None:
311+
# Cross-repo importer lookup is package-level (whole-repo, not per-file —
312+
# _find_cross_repo_importers ignores its file_path arg). Attaching one
313+
# whole-repo result across a multi-file batch would either drop the
314+
# evidence (the old `""` path) or imply per-file precision that does not
315+
# exist. Fail closed and point the caller at singular calls (#339).
316+
if cross_repo and len(file_paths) > 1:
317+
return {
318+
"error": (
319+
"cross_repo=true is not supported with a multi-file 'file_paths' "
320+
"batch: cross-repo importer lookup is package-level, not per-file. "
321+
"Use singular 'file_path' calls for cross-repo evidence."
322+
),
323+
"_meta": {
324+
"cross_repo_scope": "package",
325+
"file_count": len(file_paths),
326+
},
327+
}
308328
result = _find_importers_batch(file_paths, index, max_results, owner, name, start)
309329
if cross_repo:
330+
# len(file_paths) == 1 here — equivalent to the singular cross-repo path.
310331
try:
311332
from .list_repos import list_repos
312333
all_repos = list_repos(storage_path=storage_path).get("repos", [])
313334
cross_results = _find_cross_repo_importers(
314-
file_paths[0] if len(file_paths) == 1 else "",
315-
repo_id, all_repos, store, owner, name,
335+
file_paths[0], repo_id, all_repos, store, owner, name,
316336
)
317337
if cross_results and "results" in result:
318338
# Attach cross-repo results to the batch response

0 commit comments

Comments
 (0)