Skip to content

Commit ce8bb3f

Browse files
jgravelleclaude
andcommitted
v1.108.76: Phase 3 WI-3.1 — CI lint gate (ruff) + coverage floor
Adds the quality gates that stop debt from landing. - New `lint` CI job runs `ruff check src/` once (platform-independent). [tool.ruff.lint] keeps high-signal pyflakes + pycodestyle-error rules and grandfathers deliberate patterns (E402 lazy imports; E701/E721/E731/E741); __init__.py re-exports exempt from F401. ruff added to the dev group. - Coverage floor: the test job now runs --cov-fail-under=74 (current ~76%). - Applied ruff's safe autofixes across 49 modules (83 findings: unused imports, empty f-strings, redefinitions, safe dead-code). No behavior change; full suite 4717 passed. - Two real findings fixed: server.py annotated a local with an undefined IO (now imported from typing); storage/sqlite_store.py had a duplicate ".sh" dict key (both -> "bash"; redundant one removed). Deferred ratchets (documented): F841 (~31 pre-existing dead locals) stays ignored pending per-case RHS review; mypy/pyright report-mode is the next step. WI-2.4 (ruff BLE/TRY) intentionally not enabled — it fights the codebase's broad-except-with-logging convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 38a7080 commit ce8bb3f

53 files changed

Lines changed: 122 additions & 93 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: uv sync --group dev --extra watch
3333

3434
- name: Run tests
35-
run: uv run pytest tests/ -v --tb=short --cov=src --cov-report=term-missing
35+
run: uv run pytest tests/ -v --tb=short --cov=src --cov-report=term-missing --cov-fail-under=74
3636

3737
- name: Verify sdist contains no sensitive paths
3838
if: runner.os == 'Linux'
@@ -50,3 +50,26 @@ jobs:
5050
exit 1
5151
fi
5252
echo "OK: No sensitive paths found in sdist."
53+
54+
lint:
55+
# Platform-independent, so run once rather than across the test matrix.
56+
runs-on: ubuntu-latest
57+
steps:
58+
- uses: actions/checkout@v5
59+
60+
- name: Install uv
61+
uses: astral-sh/setup-uv@v7
62+
with:
63+
version: "0.6.5"
64+
65+
- name: Set up Python
66+
run: uv python install 3.12
67+
68+
- name: Install dependencies
69+
run: uv sync --group dev
70+
71+
- name: Ruff lint (src/)
72+
# Rule selection + grandfathered ignores live in [tool.ruff.lint] in
73+
# pyproject.toml. Keep the gate green: fix the finding, or justify a new
74+
# ignore in the same PR.
75+
run: uv run ruff check src/

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.76] - 2026-06-22 - Quality gates: CI lint (ruff) + coverage floor
6+
7+
### CI / Tooling (WI-3.1 / F-Q01)
8+
9+
- **Ruff lint gate.** New `lint` CI job runs `ruff check src/` once
10+
(platform-independent). `[tool.ruff.lint]` in pyproject keeps the high-signal
11+
pyflakes + pycodestyle-error rules and grandfathers this codebase's deliberate
12+
patterns via `ignore` (E402 lazy/local imports, plus E701/E721/E731/E741);
13+
`__init__.py` re-exports are exempt from F401. ruff added to the dev group.
14+
- **Coverage floor.** The test job now runs `--cov-fail-under=74` (current
15+
coverage ~76%), so coverage can no longer erode silently.
16+
17+
### Fixed (lint cleanup across src/)
18+
19+
- Applied ruff's safe autofixes across 49 modules (83 findings: unused imports,
20+
empty f-strings, redefinitions, and safe dead-code removals). No behavior
21+
change; full suite still 4717 passed.
22+
- Two real findings: `server.py` annotated a local with an undefined `IO` (now
23+
imported from typing — harmless at runtime since local annotations aren't
24+
evaluated, but a genuine undefined name); `storage/sqlite_store.py` had a
25+
duplicate `".sh"` dict key (both mapped to "bash"; the redundant one removed).
26+
27+
### Deferred (tracked ratchet follow-ups)
28+
29+
- F841 (~31 pre-existing dead local variables) stays in `ignore` until each can
30+
be removed with per-case RHS side-effect review. mypy/pyright type-checking is
31+
not yet wired (469 untyped modules); a report-mode step is the next ratchet.
32+
These complete Phase 3.
33+
534
## [1.108.75] - 2026-06-22 - Quality gates: live core_compact ceiling + schema/dispatch param parity
635

736
### Tests

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.76 — Phase 3 of the maintenance PRD (`jcodemunch-mcp-prd.md`), WI-3.1 (CI quality gates: ruff lint + coverage floor; folds in WI-2.4's intent). **Ruff gate:** new `lint` CI job in `.github/workflows/test.yml` runs `ruff check src/` once (platform-independent, dedicated job so it gates without 8x matrix redundancy). `[tool.ruff.lint]` in pyproject KEEPS high-signal pyflakes(F)+pycodestyle-error(E9) rules and `ignore`s this codebase's deliberate patterns: **E402** (lazy/local imports for startup speed + pytest.importorskip), E701/E721/E731/E741 (minor style); per-file-ignore `"__init__.py"=["F401"]` for re-export surfaces (though ruff already respected `__all__`, so no __init__ was touched). **F841 (~31 pre-existing dead locals) is ignored as a tracked ratchet** — safe removal needs per-case RHS side-effect review. ruff>=0.6 added to dev group. **Coverage floor:** test job now `--cov-fail-under=74` (current ~75.6%/"76%"; 74 gives headroom vs per-job variance to avoid flaky false-fails). **Lint cleanup:** `ruff check src/ --fix` applied 83 safe autofixes across 49 modules (unused imports/F401×65, dead vars, empty f-strings, redefinitions) — full suite still 4717 passed, no behavior change. **Two real findings fixed:** server.py `Optional[IO]` used undefined `IO` (added to `from typing import IO,...`; harmless at runtime — local annotations aren't evaluated — but a genuine undefined name), and `storage/sqlite_store.py` had a duplicate `".sh"` dict key (both → "bash"; redundant one removed). **WI-2.4 (ruff BLE/TRY) deliberately NOT enabled** — the codebase's intentional pattern is broad-except-WITH-logging, which BLE001 flags wholesale; enforcing it would fight the "log every silent except" convention rather than the silent-except it targets. **Phase 3 substantive work (WI-3.1/3.2/3.3) DONE; remaining ratchets = F841 cleanup + mypy/pyright report-mode step (both noted, not yet authorized as their own work).**
45
- **Version:** 1.108.75 — Phase 3 of the maintenance PRD (`jcodemunch-mcp-prd.md`, "Strengthen quality gates"), WI-3.2 + WI-3.3 (test-only, no production change). **WI-3.3 (F-Q03):** new `test_live_core_compact_under_4000_hard_ceiling` in test_schema_budget.py recomputes core+compact from `_build_tools_list()` and hard-asserts ≤4000 on the LIVE build — the existing test only read the frozen `schema_baseline.json`, so a breach (e.g. v1.108.70's transient 3956→4011) shipped until the baseline was regenerated. **WI-3.2 (F-P02/F-C02):** new `tests/test_dispatch_schema_parity.py` — the existing registration guard (test_config.py:1740) only checks tool-NAME presence in `_CANONICAL_TOOL_NAMES`, not params; this parses `call_tool`'s source (AST), collects every `arguments["k"]`/`.get("k")` each `if/elif name=="<tool>"` branch reads, and asserts each is a declared inputSchema property (so a caller can pass it). It FOUND two intentional patterns, now handled: config-conditional props (`render_diagram.open_in_viewer`, gated on `render_diagram_viewer_enabled` — test builds the schema with the flag ON) + a forgiving alias (`get_file_outline` accepts `file` for `file_path` — `_KNOWN_FORGIVING_ALIASES`). Guard-the-guard test (`>50 branches found`) prevents the AST walk going vacuous. Full suite 4717 passed. **Remaining Phase 3: WI-3.1 (CI gates: ruff lint+format, mypy/pyright report-mode, `--cov-fail-under`, folds in WI-2.4 ruff BLE/TRY) = next, the bigger one (lint cleanup across 469 modules).** Note: jcm git-HEAD `7762b73` was the v1.108.74 base; server.py line numbers in the stale jcm index drift after every server.py edit — reindex or use inspect/Read offset for precise positions.
56
- **Version:** 1.108.74 — Phase 2 of the maintenance PRD (`jcodemunch-mcp-prd.md`, "Harden"), WI-2.2 (F-P01). **Tool errors now carry `isError`.** Errors were returned in-band only (`{"error": ...}` JSON body in a plain content list → SDK wraps `CallToolResult(isError=False)`), so a non-Claude MCP client that branches on `isError` saw a failure as success — the exact risk for a custom HTTP harness (the Darius case). `call_tool` (server.py:4129) now returns `CallToolResult(isError=True)` for every failure path via new `_error_call_result(text)` helper: input-validation (jsonschema), search_text arg-validation, project-disabled-tool, the common in-band error branch at the JSON return (`isinstance(result,dict) and "error" in result` — covers in-band tool errors + "Unknown tool"), internal-KeyError, missing-argument, and the generic exception handler. The SAME JSON body rides in `content` (v1.108.30 in-band contract preserved); SUCCESS stays a plain `list[TextContent]` (SDK wraps isError=False) → additive on the wire, only failures gain the signal. The SDK supports this directly (`elif isinstance(results, CallToolResult): return ServerResult(results)`), so no raise (raising would mangle the body). The `route` front door (`_handle_route`) updated to surface a routed action's error under its envelope instead of `list()`-ing a non-iterable CallToolResult; `order` passes through naturally. **In-process blast radius (the real cost):** every test that subscripted an error result as a list (`result[0].text`) broke — fixed across test_server.py, test_tier_runtime.py (added `_rtext()` shape-robust reader), test_counter.py (`_call` helper), test_v1_108_55/56.py. New `tests/test_v1_108_74.py` (4: unknown-tool isError + body preserved, input-validation isError, disabled-tool isError, success-stays-plain-list). Full suite 4714 passed/10 skipped. **WI-2.4 (ruff BLE/TRY) remains, deferred to pair with Phase 3 WI-3.1 (CI lint job) — so Phase 2's substantive work is DONE; only the CI-coupled lint rule is left for Phase 3.**
67
- **Version:** 1.108.73 — Phase 2 of the maintenance PRD (`jcodemunch-mcp-prd.md`, "Harden"), WI-2.1 + WI-2.3. **WI-2.1 (F-S01): HTTP ingest write endpoints fail closed without a token.** `BearerAuthMiddleware` (server.py) only enforces `JCODEMUNCH_HTTP_TOKEN` *when set*, so enabling `runtime_ingest_enabled`/`org_ingest_enabled` with no token left an unauthenticated write surface guarded only by a startup warning (warn-only). jjg ruled **fail-closed-always**: new shared `_ingest_auth_error()` in `runtime/http_routes.py` (reads the same env source the middleware reads) returns 503 when an ingest endpoint is enabled but no token is set, wired into `_shared_handler_setup` (all 3 runtime routes) + imported into `org/http_routes.handle_org_report`. Disabled path unchanged (rejects on the enable check, before auth). Verified the other two F-S01 guards still hold: gzip-bomb (`_read_body` checks decompressed size separately, post-inflate) + per-repo write lock (`_RepoLockRegistry` + `_run_with_lock`). Residual (noted, not fixed): `_read_body` does `gzip.decompress` fully before the size check (decompress-then-check, not bounded streaming) — low priority, off-by-default endpoint, 5MB cap. **WI-2.3 (F-S03): redaction currency.** Added structural `redact.py` patterns for `github_pat_` (fine-grained GitHub PAT; classic `gh[pousr]_` doesn't match it), `sk-proj-`/`sk-` (OpenAI; bare branch forbids a hyphen so it can't swallow sk-ant-), `sk-ant-` (Anthropic, listed first so it's labelled+redacted before the OpenAI scan). New `tests/test_v1_108_73.py` (10); the runtime/org happy-path fixtures (`test_runtime_phase6.py`, `test_org_http.py`) now set a token (required by fail-closed). Full suite 4710 passed/10 skipped. **WI-2.2 (isError, has a Darius tie-in — he's a non-Claude HTTP client) + WI-2.4 (ruff BLE/TRY, deferred to pair with Phase 3 WI-3.1) remain in Phase 2.**

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ is a byte the agent doesn't pay to read.
110110
<!-- WHATSNEW:START -->
111111
#### What's new
112112

113+
- **[v1.108.76](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.76)** (2026-06-22) — Quality gates: CI lint (ruff) + coverage floor
113114
- **[v1.108.75](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.75)** (2026-06-22) — Quality gates: live core_compact ceiling + schema/dispatch param parity
114115
- **[v1.108.74](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.74)** (2026-06-22) — Harden: tool errors carry isError (MCP protocol conformance)
115-
- **[v1.108.73](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.73)** (2026-06-22) — Harden: HTTP ingest fails closed without a token + redaction currency
116116
<!-- WHATSNEW:END -->
117117

118118
![License](https://img.shields.io/badge/license-dual--use-blue)

pyproject.toml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.75"
3+
version = "1.108.76"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -87,10 +87,36 @@ exclude = [".claude/", "index.php"]
8787
testpaths = ["tests"]
8888
asyncio_mode = "auto"
8989

90+
[tool.ruff]
91+
target-version = "py310"
92+
93+
[tool.ruff.lint]
94+
# Default rule set is pycodestyle E4/E7/E9 + pyflakes F. We keep the
95+
# high-signal correctness rules (unused imports, undefined names, duplicate
96+
# keys, f-string/format bugs, redefinitions, comparison/syntax errors) and
97+
# ignore the codes that reflect deliberate, pervasive patterns in this
98+
# codebase rather than real defects.
99+
ignore = [
100+
"E402", # module-import-not-at-top: deliberate lazy/local imports (startup speed, pytest.importorskip)
101+
"E701", # multiple-statements-on-one-line: a few terse one-line guards
102+
"E721", # type-comparison: a handful of intentional type(x) == Y checks
103+
"E731", # lambda-assignment: stylistic
104+
"E741", # ambiguous variable name (l/I/O): pre-existing, low risk
105+
# F841 (local assigned but never used): ~31 pre-existing dead locals. Safe
106+
# removal needs per-case RHS side-effect review, so it is a tracked ratchet
107+
# follow-up rather than a gate blocker. Re-enable once cleaned.
108+
"F841",
109+
]
110+
111+
[tool.ruff.lint.per-file-ignores]
112+
# __init__.py re-exports are the package's public API surface, not dead imports.
113+
"__init__.py" = ["F401"]
114+
90115
[dependency-groups]
91116
dev = [
92117
"pytest>=9.0.2",
93118
"pytest-asyncio>=1.3.0",
94119
"pytest-cov>=7.0.0",
95120
"hypothesis>=6.0.0",
121+
"ruff>=0.6.0",
96122
]

src/jcodemunch_mcp/cli/receipt.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import datetime as _dt
2020
import io
2121
import json
22-
import os
2322
import sys
2423
from pathlib import Path
2524
from typing import Iterable, Optional

src/jcodemunch_mcp/cli/skills.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import shutil
2525
from pathlib import Path
26-
from typing import Optional
2726

2827

2928
# Marker is on the first non-frontmatter heading line so we can detect an

src/jcodemunch_mcp/counter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from __future__ import annotations
3333

3434
import re
35-
from typing import Any, Iterable, Optional
35+
from typing import Iterable, Optional
3636

3737
# The front-door tool names. These are never themselves dispatchable via
3838
# ``order`` (no front-door recursion).

src/jcodemunch_mcp/embeddings/local_encoder.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
``onnxruntime`` is installed but the model file is missing.
1414
"""
1515

16-
import json
1716
import logging
1817
import os
19-
import re
2018
import sys
2119
import unicodedata
2220
from pathlib import Path

src/jcodemunch_mcp/enrichment/lsp_bridge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import subprocess
3535
import threading
3636
import time
37-
from dataclasses import dataclass, field
37+
from dataclasses import dataclass
3838
from pathlib import Path
3939
from typing import Any, Optional
4040

0 commit comments

Comments
 (0)