|
1 | 1 | # jcodemunch-mcp — Project Brief |
2 | 2 |
|
3 | 3 | ## Current State |
4 | | -- **Version:** 1.108.180 — **Unknown freshness stops collapsing into fresh (#377 hardening item 4).** ⚠ **`FreshnessProbe.repo_is_stale` is a BOOLEAN and a boolean has nowhere to put "I could not find out": it returned False BOTH when the SHAs matched AND when either was missing, and the verdict rendered False as `channels.index: "fresh"` — so an index whose freshness was NEVER ESTABLISHED claimed current-snapshot equivalence, and every absence gate downstream trusted a comparison that was never made.** New `FreshnessProbe.repo_freshness` → `fresh`/`stale`/`unknown`/`not_tracked`; `build_verdict(freshness=)` renders it into `channels.index`. ⚠ **`unknown` and `not_tracked` get OPPOSITE treatment on purpose: `unknown` = a capability we HAVE, failing (git absent, source root moved, index stored no SHA) ⇒ zero-result scan is `degraded`, absence REFUSED. `not_tracked` = a capability the SUBJECT does not support (a plain indexed folder has no revision, ever) ⇒ DISCLOSED, absence still citable — the jdata v1.26.0 call. Refusing there would strip absence evidence from EVERY folder index over a permanent, already-stated limitation unrelated to whether the target exists.** ⚠ **A SUBDIRECTORY of a checkout is tracked by the checkout ABOVE it — trackability walks up the way git does (`_is_git_backed`), else every monorepo subdir index silently understates to `not_tracked`.** Precedence `rebuilding` > `partial` > `stale` > `unknown`/`not_tracked` > `fresh`. ⚠ **The `channels.index` enum is CLOSED in the published schema — both new values had to be added or every affected response fails our own contract (the 1.108.168 trap, third occurrence).** Zero blast radius without `freshness=` (pinned); an unrecognized value is ignored, not propagated. New `tests/test_v1_108_180.py` (26) incl. a REAL one-commit checkout, because `fresh` is the one state a fixture cannot fake. No tool-count/INDEX_VERSION change. |
| 4 | +- **Version:** 1.108.181 — **Uncommitted work is represented, PER SCOPE (#377 hardening item 5 — closes the last open 1.x item).** ⚠ **Git HEAD sits still while the tree holds a modified file, a brand-new UNTRACKED implementation, a deletion or a rename into/out of scope — so every freshness gate we had could report `fresh` over a corpus that never read the file the target lives in. A scan that RETURNS rows carries per-file freshness ON them; a scan that returns NOTHING has nowhere to put it.** New `subject_state.working_tree_state` gives the SCOPE its own state (`clean`/`dirty_in_scope`/`dirty_outside_scope`/`unknown`/`not_applicable`) as `verdict.working_tree`, wired into `search_symbols` / `search_text` / BOTH `get_ranked_context` exits. ⚠ **TWO restrictions are what keep it from becoming noise, and both are load-bearing: (1) work OUTSIDE the scanned scope NEVER blocks (his own distinction — a dirty file elsewhere cannot invalidate a narrow proof); (2) an edit the index has ALREADY re-read is NOT a gap — `files_not_in_index` compares each dirty in-scope path against `index.file_mtimes`, so a watcher-fresh corpus keeps proving absence. Without (2) the gate fires on EVERY developer with unsaved work in an up-to-date repo, and a signal that fires constantly is one people learn to ignore.** ⚠ **Measured for a ZERO-RESULT scan only** (a `git status` per search would tax the common path); reading is TTL-cached and SHARED with the 1.108.178 cached-negative revalidation. ⚠ **Rename handling: `R old -> new` records the NEW path — the one that exists in the tree and the one a search would have had to see;** quoted/backslashed paths normalised to compare against index paths. `working_tree` published in the verdict schema. New `tests/test_v1_108_181.py` (27). No tool-count/INDEX_VERSION change. |
| 5 | +- **Prior (1.108.180):** — **Unknown freshness stops collapsing into fresh (#377 hardening item 4).** ⚠ **`FreshnessProbe.repo_is_stale` is a BOOLEAN and a boolean has nowhere to put "I could not find out": it returned False BOTH when the SHAs matched AND when either was missing, and the verdict rendered False as `channels.index: "fresh"` — so an index whose freshness was NEVER ESTABLISHED claimed current-snapshot equivalence, and every absence gate downstream trusted a comparison that was never made.** New `FreshnessProbe.repo_freshness` → `fresh`/`stale`/`unknown`/`not_tracked`; `build_verdict(freshness=)` renders it into `channels.index`. ⚠ **`unknown` and `not_tracked` get OPPOSITE treatment on purpose: `unknown` = a capability we HAVE, failing (git absent, source root moved, index stored no SHA) ⇒ zero-result scan is `degraded`, absence REFUSED. `not_tracked` = a capability the SUBJECT does not support (a plain indexed folder has no revision, ever) ⇒ DISCLOSED, absence still citable — the jdata v1.26.0 call. Refusing there would strip absence evidence from EVERY folder index over a permanent, already-stated limitation unrelated to whether the target exists.** ⚠ **A SUBDIRECTORY of a checkout is tracked by the checkout ABOVE it — trackability walks up the way git does (`_is_git_backed`), else every monorepo subdir index silently understates to `not_tracked`.** Precedence `rebuilding` > `partial` > `stale` > `unknown`/`not_tracked` > `fresh`. ⚠ **The `channels.index` enum is CLOSED in the published schema — both new values had to be added or every affected response fails our own contract (the 1.108.168 trap, third occurrence).** Zero blast radius without `freshness=` (pinned); an unrecognized value is ignored, not propagated. New `tests/test_v1_108_180.py` (26) incl. a REAL one-commit checkout, because `fresh` is the one state a fixture cannot fake. No tool-count/INDEX_VERSION change. |
5 | 6 | - **Prior (1.108.179):** — **The subject has to hold still for the scan (#377 hardening item 6).** `search_symbols` / `search_text` / `get_ranked_context` now capture subject identity BEFORE retrieval and compare AFTER: a concurrent edit, watcher reindex, incremental save, published generation or a long scan crossing a rebuild all mean the scan describes NEITHER the state it started against NOR the one it finished in. Zero-result + movement ⇒ `degraded` + `verdict.moved_during_scan` naming what moved. ⚠ **Scoped to a ZERO-RESULT scan, and that scoping is what makes it affordable: the AFTER reading bypasses the TTL-cached HEAD lookup and pays for a real `git rev-parse`, because a before/after comparison INSIDE the 2s cache window compares one reading with itself.** A scan that returned rows read them from a generation that really held them, and the freshness channels already disclose the tree moved. ⚠ **Second find, worse than the one being fixed: `get_ranked_context`'s no-candidate EARLY RETURN carried NO verdict at all** — it asserted "No implementation found ... Do not claim this feature exists" in PROSE with no freshness, no coverage, no scan counts and none of the absence gates, because it returned before `build_verdict` was ever called. Now builds the same verdict every other retrieval answer gets (so a genuine no-candidate result is CITABLE, and a stale/partial/rebuilding/moving one is REFUSED). `subject_state.changed(when=)` now distinguishes the two callers' sentences: "after this scan was cached" (1.108.178 cache replay) vs "while the scan was running" (this). ⚠ **Working-tree movement DURING a scan is still NOT checked** — the BEFORE reading would have to pay `git status` on every search; whole-scope tree state is item 5. `moved_during_scan` published in the verdict schema. New `tests/test_v1_108_179.py` (19), all three tools in one parametrized e2e. No tool-count/INDEX_VERSION change. |
6 | | -- **Prior (1.108.178):** — **A cached negative must prove it still describes the subject (#377 hardening item 3).** ⚠ **The `search_symbols` result cache keys on `indexed_at`, so it invalidates on a REINDEX and NOTHING ELSE: a query absent against snapshot A cached its `fresh`/`absent` verdict, the source changed WITHOUT a rebuild, the same query hit the cache, and the old verdict was replayed — minting an absence proof against a state nobody scanned.** New `retrieval/subject_state.py` captures index generation + `.db` mtime + live git HEAD (+ the working-tree fingerprint for an absence) at cache-WRITE time and re-checks at cache-READ time; on a move it discloses `verdict.revalidated`, downgrades `absent` → `degraded`, and STRIPS any token minted on the earlier pass (it was issued against the state that just failed to hold). ⚠ **Only a NEGATIVE pays for `git status` — the one answer a working-tree edit can falsify while the index and its generation sit still. A cached positive costs a few stats and keeps serving with disclosure: its rows were really in the index at that generation.** ⚠ **UNKNOWN is never reported as CHANGED** (a non-git checkout knows none of this and did not change either; treating unknown as moved would refuse every absence there — same shape as tri-state `coverage.complete`). ⚠ **Second bug fixed in passing: `_result_cache_get` copied the result and `_meta` but NOT the nested `verdict`, so the `evidence_ref` the DISPATCHER attaches after the tool returns landed in the STORED entry and was replayed to every later hit.** ⚠ **Verified on a REAL checkout, not just fixtures: unchanged tree replays `absent` identically; an untracked file appearing flips the next hit to `degraded` + `revalidated.stale_cache`.** Working-tree state is scoped to the CACHED-negative path here — whole-scope semantics for a LIVE absence is item 5, NOT in this release. `revalidated` published in the verdict schema. New `tests/test_v1_108_178.py` (20). No tool-count/INDEX_VERSION change. |
7 | | -- **Older releases (1.108.177 and earlier):** see `CHANGELOG.md` — the authoritative version history (415 entries, 1.0.0 onward). |
| 7 | +- **Older releases (1.108.178 and earlier):** see `CHANGELOG.md` — the authoritative version history (416 entries, 1.0.0 onward). |
8 | 8 | - **INDEX_VERSION:** 17 |
9 | | -- **Tests:** 5897 passed, 7 skipped (1.108.180) + the KNOWN 12 local-ONNX `test_semantic_search` env failures (green in CI on all 4 ubuntu jobs — never read them as a regression) |
| 9 | +- **Tests:** 5924 passed, 7 skipped (1.108.181) + the KNOWN 12 local-ONNX `test_semantic_search` env failures (green in CI on all 4 ubuntu jobs — never read them as a regression) |
10 | 10 | - **Python:** >=3.10 |
11 | 11 | - **Tool count:** 90 in `full` (front door hidden; +1 v1.108.111 `get_parity_map`, +1 v1.108.112 `get_decorator_census`, +1 v1.108.113 `get_architecture_metrics`); `tool_surface=counter` exposes a 3-tool front door (`order`/`menu`/`route`) instead |
12 | 12 |
|
@@ -50,7 +50,7 @@ src/jcodemunch_mcp/ |
50 | 50 | enrichment/ |
51 | 51 | lsp_bridge.py # LSP bridge — opt-in compiler-grade call graph resolution via pyright/gopls/ts-language-server/rust-analyzer; LSPServer lifecycle, LSPBridge multi-server manager, enrich_call_graph_with_lsp() + enrich_dispatch_edges() (interface/trait dispatch resolution) |
52 | 52 | retrieval/ |
53 | | - subject_state.py # (v1.108.178) #377 item 3: what a scan's answer depends on, cheap enough to re-check. capture() at cache-WRITE (index generation, .db mtime, live git HEAD, + working-tree fingerprint ONLY for an absence) / changed() at cache-READ / revalidate_verdict() downgrades a replayed `absent` and strips the stale evidence token. UNKNOWN is never a change. v1.108.179 adds moved_during_scan() (item 6: before/after identity around a scan, fresh_head bypasses the TTL cache) + changed(when=) so the cached-replay and live-scan refusals read differently |
| 53 | + subject_state.py # (v1.108.178) #377 item 3: what a scan's answer depends on, cheap enough to re-check. capture() at cache-WRITE (index generation, .db mtime, live git HEAD, + working-tree fingerprint ONLY for an absence) / changed() at cache-READ / revalidate_verdict() downgrades a replayed `absent` and strips the stale evidence token. UNKNOWN is never a change. v1.108.179 adds moved_during_scan() (item 6: before/after identity around a scan, fresh_head bypasses the TTL cache) + changed(when=) so the cached-replay and live-scan refusals read differently. v1.108.181 adds working_tree_state() (item 5: scope-level clean/dirty_in_scope/dirty_outside_scope/unknown/not_applicable; blocks ONLY on in-scope dirt the index has not re-read) + _parse_porcelain/_in_scope/_unreflected_in_index |
54 | 54 | signal_fusion.py # Weighted Reciprocal Rank (WRR) fusion: lexical + structural + similarity + identity channels |
55 | 55 | regret.py # (v1.108.68) analyze_regret: mines the ranking_events ledger for SIX retrieval-regret signals (requery_churn/low_confidence/thin_result/ambiguous_top/stale_at_query/vocabulary_gap) as severity-ranked clusters. Pure read via token_tracker.ranking_db_query; no new tables. Consumed by suggest_corrections + the digest one-liner |
56 | 56 | summarizer/ |
|
0 commit comments