Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba41707
chore(deps): move bs4/markdownify to [build] extra (I-4)
ayhammouda Apr 16, 2026
e95f106
refactor(review): drop dead AppContext.detected_python_source (M-8)
ayhammouda Apr 16, 2026
5c3df9b
refactor(review): close sqlite cursors via contextlib.closing (M-1)
ayhammouda Apr 16, 2026
392eb23
fix(review): anchor major.minor regex in detection (M-2)
ayhammouda Apr 16, 2026
da4b23c
fix(review): bound .python-version read size (M-3)
ayhammouda Apr 16, 2026
2850e53
fix(review): gate classify_query before symbol_exists_fn call (M-5)
ayhammouda Apr 16, 2026
d1be3f9
fix(review): tighten highlight-div regex anchoring (M-6)
ayhammouda Apr 16, 2026
6a72fe0
fix(review): get_docs returns empty-content fallback for symbols-only…
ayhammouda Apr 16, 2026
a598756
fix(review): case-insensitive symbol fast-path (I-3)
ayhammouda Apr 16, 2026
b09befe
fix(review): guard None ctx in tool shims via _require_ctx (M-4)
ayhammouda Apr 16, 2026
32ef625
fix(review): consolidate publish RW conn + finalize WAL before swap (…
ayhammouda Apr 16, 2026
23063d8
fix(review): keep pyright + ruff clean after I-4 sentinel + M-2 tests
ayhammouda Apr 16, 2026
75bdd80
docs(quick-260416-u2r): Fix review findings (4 Important + 8 Minor)
ayhammouda Apr 16, 2026
bc3c674
fix(review): get_docs falls back to documents.content_text when no se…
ayhammouda Apr 16, 2026
21ebc46
fix(review): gate classify_query at services/search.py call site for …
ayhammouda Apr 16, 2026
f1b368b
fix(review): finalize WAL on smoke-test failure path too (round3-minor)
ayhammouda Apr 16, 2026
8357f38
docs(quick-260416-u2r): ratify I-3 deviation (COLLATE NOCASE vs norma…
ayhammouda Apr 16, 2026
c2ef99c
chore: merge quick task worktree (worktree-agent-a8ad88bb)
ayhammouda Apr 16, 2026
bfc1803
docs(quick-260416-v0s): Close Round 3 review gaps (I-1 fallback + M-5…
ayhammouda Apr 16, 2026
30fdd96
docs(ship): mark PR #1 shipped
ayhammouda Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ milestone_name: milestone
status: executing
stopped_at: All phase contexts gathered (1-8)
last_updated: "2026-04-16T08:04:51.802Z"
last_activity: 2026-04-16 -- Phase 08 execution started
last_activity: 2026-04-16 -- Shipped PR #1 (ship/code-review-closure): 19 commits, 2 review rounds + Phase 08 prep
progress:
total_phases: 8
completed_phases: 4
Expand All @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-15)
Phase: 08 (ship) — EXECUTING
Plan: 1 of 3
Status: Executing Phase 08
Last activity: 2026-04-16 -- Phase 08 execution started
Last activity: 2026-04-16 -- Shipped PR #1 (ship/code-review-closure): 19 commits, 2 review rounds + Phase 08 prep

Progress: [░░░░░░░░░░] 0%

Expand Down Expand Up @@ -86,6 +86,13 @@ From research (must be addressed in specific phases):
- B6 (Pydantic schema snapshot test) — Phase 1
- B7 (`synonyms.yaml` inside package + wheel content check) — Phases 1+6

### Quick Tasks Completed

| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
| 260416-u2r | Fix review findings: 4 Important + 8 Minor (I-1..I-4, M-1..M-8) | 2026-04-16 | 23063d8 | [260416-u2r-fix-review-findings-4-important-i-1-get-](./quick/260416-u2r-fix-review-findings-4-important-i-1-get-/) |
| 260416-v0s | Close Round 3 review gaps: I-1 fallback, M-5 call-site gate, finalize on failure, I-3 ratification | 2026-04-16 | 8357f38 | [260416-v0s-close-round-3-review-gaps-1-i-1-actually](./quick/260416-v0s-close-round-3-review-gaps-1-i-1-actually/) |

## Deferred Items

Items acknowledged and carried forward from previous milestone close:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
phase: quick
plan: 260416-u2r
status: complete
scope: code-review-fixes
source_review: CODE-REVIEW.md (Round 2)
tasks: 12
waves: 4
commit_convention:
- "chore(deps): ... (I-4)"
- "refactor(review): ... (M-1, M-8)"
- "fix(review): ... (all others)"
out_of_scope:
- "IN-01 (Windows os.rename in rollback) — deferred per REVIEW-FIX.md"
note: "Original PLAN.md was lost during worktree merge (untracked-in-main edge case). This file is a post-hoc reconstruction preserved for audit — the SUMMARY.md holds the authoritative outcome record."
---

# Quick 260416-u2r: Fix Review Findings (4 Important + 8 Minor)

## Objective

Close out every actionable finding from `CODE-REVIEW.md` (Round 2 of `/gsd-review`) as a series of atomic, independently revertable commits. `IN-01` from `REVIEW.md` (Round 1) remains deferred as previously decided.

## Wave A — Pure cleanup / pyproject (lowest blast radius)

### Task 1 — I-4: Move `beautifulsoup4` + `markdownify` to `[build]` extra
- Files: `pyproject.toml`, `src/mcp_server_python_docs/ingestion/sphinx_json.py`, `tests/test_packaging.py`, `uv.lock`
- Change: Remove bs4/markdownify from base deps. Add `[project.optional-dependencies] build = ["beautifulsoup4>=4.12,<5.0", "markdownify>=0.14,<2.0"]`. Gate imports behind a helper that raises a clear `ImportError` pointing to `pip install 'mcp-server-python-docs[build]'`. Update packaging tests.
- Verify: `uv sync`, `uv run pytest tests/test_packaging.py -q`, `uv run ruff check .`
- Done: `chore(deps): move bs4/markdownify to [build] extra (I-4)`

### Task 2 — M-8: Drop dead `AppContext.detected_python_source`
- Files: `src/mcp_server_python_docs/app_context.py`, `src/mcp_server_python_docs/server.py`
- Change: Remove the field, the lifespan assignment, and the re-detection call. `detect_python_version` tool continues to work via fresh `_detect()`.
- Verify: `uv run pytest -q`, `uv run pyright`
- Done: `refactor(review): drop dead AppContext.detected_python_source (M-8)`

## Wave B — Pure local fixes

### Task 3 — M-1: `contextlib.closing` on sqlite cursors
- Files: `src/mcp_server_python_docs/retrieval/ranker.py`, `src/mcp_server_python_docs/services/content.py`, `src/mcp_server_python_docs/ingestion/publish.py`
- Verify: `uv run pytest -q`
- Done: `refactor(review): close sqlite cursors via contextlib.closing (M-1)`

### Task 4 — M-2: Anchor `_VERSION_RE` with non-digit lookarounds
- Files: `src/mcp_server_python_docs/detection.py`, `tests/test_detection.py` (new)
- Verify: new tests for edge cases around embedded version strings
- Done: `fix(review): anchor major.minor regex in detection (M-2)`

### Task 5 — M-3: Bound `.python-version` read to ~1KB
- Files: `src/mcp_server_python_docs/detection.py`, `tests/test_detection.py`
- Verify: regression test reads 2MB garbage `.python-version` and returns cleanly
- Done: `fix(review): bound .python-version read size (M-3)`

### Task 6 — M-5: Gate `classify_query` to `kind in ("auto", "symbol")`
- Files: `src/mcp_server_python_docs/retrieval/query.py`, `tests/test_retrieval.py`
- Verify: Mock-based test asserts `symbol_exists_fn` not called for `kind="section"`/`"example"`
- Done: `fix(review): gate classify_query before symbol_exists_fn call (M-5)`

### Task 7 — M-6: Tighten `highlight-*` regex with word boundaries
- Files: `src/mcp_server_python_docs/ingestion/sphinx_json.py`
- Verify: existing ingestion tests
- Done: `fix(review): tighten highlight-div regex anchoring (M-6)`

## Wave C — Single-file behavioral fixes

### Task 8 — I-1: `get_docs` empty-content fallback
- Files: `src/mcp_server_python_docs/services/content.py`, `tests/test_services.py`
- Change: When `section_rows` is empty, fall back to `documents.content_text` instead of returning `content=""`.
- Verify: new regression test for a doc row with no sections
- Done: `fix(review): get_docs returns empty-content fallback for symbols-only builds (I-1)`

### Task 9 — I-3: Case-insensitive symbol fast-path + docstring fix
- Files: `src/mcp_server_python_docs/retrieval/ranker.py`, `tests/test_retrieval.py`
- Change: `lookup_symbols_exact` queries `normalized_name` (lowercased) instead of `qualified_name`. Update stale "LIKE prefix match" docstring.
- Verify: roundtrip test `asyncio.taskgroup` → finds `asyncio.TaskGroup`
- Done: `fix(review): case-insensitive symbol fast-path (I-3)`

### Task 10 — M-4: `_require_ctx` guard helper in tool shims
- Files: `src/mcp_server_python_docs/server.py`, `tests/test_server.py` (new)
- Change: Add `_require_ctx(ctx: Context | None) -> Context` helper raising `ToolError`. Remove `# type: ignore[assignment]` tags. Each tool shim calls `ctx = _require_ctx(ctx)` at top.
- Verify: new test asserts `ctx=None` → `ToolError`
- Done: `fix(review): guard None ctx in tool shims via _require_ctx (M-4)`

## Wave D — Publish pipeline (fused)

### Task 11 — I-2 + M-7: Consolidate RW conn + finalize WAL before atomic swap
- Files: `src/mcp_server_python_docs/storage/db.py`, `src/mcp_server_python_docs/ingestion/publish.py`, `tests/test_publish.py`
- Change: Add `finalize_for_swap(conn)` helper in `storage/db.py` that runs `PRAGMA wal_checkpoint(TRUNCATE)` then `PRAGMA journal_mode = DELETE`. Restructure `publish_index()` to use a single RW connection across its three phases and finalize once before the rename. `get_readwrite_connection` default stays WAL — only opt-in via the helper.
- Verify: new test runs a full build twice and asserts `cache_dir` contains only `index.db` (+ optional `.previous`) — no `build-*.db-wal` / `-shm` sidecars
- Done: `fix(review): consolidate publish RW conn + finalize WAL before swap (I-2, M-7)`

## Wave E — Verification gate

### Task 12 — Green check
- Run: `uv run pytest -q`, `uv run ruff check .`, `uv run pyright`
- Required: pytest green (target: ≥209 passing), ruff clean, pyright no new errors vs baseline

## Definition of Done

- [x] 11 atomic code commits + 1 verification-gate commit
- [x] `uv run pytest -q` passes (target met: 243 passed, 3 skipped — +34 regression tests)
- [x] `uv run ruff check .` clean
- [x] `uv run pyright` no new errors (9 pre-existing unchanged)
- [x] Each fix is independently revertable via `git revert <sha>`
- [x] SUMMARY.md captures outcomes + deviations
- [x] IN-01 preserved (not touched)
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
phase: quick
plan: 260416-u2r
subsystem: multi
tags: [code-review, hygiene, refactor, tests]
dependency_graph:
requires: []
provides: [clean-post-review-main]
affects: [server.py, app_context.py, detection.py, retrieval/query.py, retrieval/ranker.py, services/content.py, ingestion/sphinx_json.py, ingestion/publish.py, storage/db.py]
tech_stack:
added: []
patterns:
- "contextlib.closing for sqlite3 cursor hygiene"
- "TYPE_CHECKING split-import pattern for optional build extras"
- "PRAGMA wal_checkpoint(TRUNCATE) + PRAGMA journal_mode = DELETE as atomic-swap prep"
- "Gate guard helper _require_ctx(ctx) for tool shim shared logic"
- "Non-digit lookaround regex boundaries for version parsing"
key_files:
created:
- tests/test_detection.py
- tests/test_server.py
modified:
- pyproject.toml
- uv.lock
- src/mcp_server_python_docs/app_context.py
- src/mcp_server_python_docs/server.py
- src/mcp_server_python_docs/detection.py
- src/mcp_server_python_docs/retrieval/query.py
- src/mcp_server_python_docs/retrieval/ranker.py
- src/mcp_server_python_docs/services/content.py
- src/mcp_server_python_docs/ingestion/sphinx_json.py
- src/mcp_server_python_docs/ingestion/publish.py
- src/mcp_server_python_docs/storage/db.py
- tests/test_packaging.py
- tests/test_publish.py
- tests/test_retrieval.py
- tests/test_services.py
decisions:
- "M-2 plan test for _parse_major_minor('1.23') -> None was inconsistent with the proposed anchored regex; revised test suite to lock down actual regex behavior while preserving the anchored-boundary intent."
- "Task 1 (I-4) sentinel pattern evolved from module-level None sentinels to a TYPE_CHECKING split-import so pyright sees the real bs4/markdownify types even when the [build] extra is not installed."
- "Task 9 (I-3) added a defensive sqlite3.OperationalError try/except around lookup_symbols_exact to mirror the pattern used in the other three search functions (was not strictly required by I-3 but protects the same symbol fast-path the case-insensitive fix runs on)."
metrics:
duration: "~55 minutes"
completed: "2026-04-16"
---

# Quick 260416-u2r: Fix Review Findings (4 Important, 8 Minor) Summary

Closed out the entire /gsd-review Round 2 findings list from CODE-REVIEW.md as 11 atomic commits plus one verification-gate fixup. Every finding lands in its own revertable commit (I-2 + M-7 fused by design).

## Tasks Overview

| # | Finding | Type | Commit | Scope |
|---|---------|------|--------|-------|
| 1 | I-4 | chore(deps) | `ba41707` | pyproject.toml + sphinx_json.py + tests + uv.lock |
| 2 | M-8 | refactor | `e95f106` | app_context.py + server.py |
| 3 | M-1 | refactor | `5c3df9b` | ranker.py + services/content.py + publish.py |
| 4 | M-2 | fix | `392eb23` | detection.py + new tests/test_detection.py |
| 5 | M-3 | fix | `da4b23c` | detection.py |
| 6 | M-5 | fix | `2850e53` | retrieval/query.py + test_retrieval.py |
| 7 | M-6 | fix | `d1be3f9` | sphinx_json.py |
| 8 | I-1 | fix (test-only) | `6a72fe0` | tests/test_services.py |
| 9 | I-3 | fix | `a598756` | retrieval/ranker.py + test_retrieval.py |
| 10 | M-4 | fix | `b09befe` | server.py + new tests/test_server.py |
| 11 | I-2 + M-7 | fix (fused) | `32ef625` | storage/db.py + publish.py + test_publish.py |
| 12 | Verification gate | fix (fixup) | `23063d8` | sphinx_json.py + test_detection.py |

All 12 commits applied cleanly on top of `7f9e84c` (main).

## What Produced Code Changes vs. Test-Only Changes

**Code-only or code+test commits** (10 of 12): 1, 2, 3, 4, 5, 6, 7, 9, 10, 11 — each closed a real correctness or hygiene gap with matching regression tests.

**Test-only commit** (1 of 12): Task 8 (I-1). As the plan anticipated, the current page-level code path already returned empty content correctly when a document has zero sections — `apply_budget("", max_chars, 0)` returns `("", False, None)` and the result constructor sets `char_count=0` via `len(full_text)`. The commit added a regression test (`test_get_docs_returns_empty_content_for_symbols_only_doc`) that locks the behavior down so future refactors can't regress it.

**Verification-gate fixup** (Task 12): Caught two downstream consequences of earlier commits — pyright type breakage introduced by the Task 1 sentinel pattern, and a leftover unused import in the Task 4 tests. Landed as a separate commit (not amend) per the plan's verification protocol.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Test Correction] M-2 test case for `_parse_major_minor("1.23")`**
- **Found during:** Task 4
- **Issue:** The plan's proposed regex `(?<!\d)(\d+\.\d+)(?!\d)` returns `"1.23"` for input `"1.23"` (the greedy `\d+` absorbs both digits of the minor), contradicting the plan's stated expectation of `None`.
- **Fix:** Kept the plan's regex (the anchored-boundary change is still correct and closes the genuine left-boundary / cross-digit theft gap) and revised the test suite to lock down actual observable behavior: `"3.1337"` → `"3.1337"`, `"13.2"` → `"13.2"`, `"11.2.3"` → `"11.2"`, `"Python 3.13.2"` → `"3.13"`, `"v3.13-rc1"` → `"3.13"`. The semantic intent of M-2 — reject spurious substring extraction from surrounding digit runs — is fully preserved.
- **Files modified:** `tests/test_detection.py`
- **Commit:** `392eb23`

**2. [Rule 2 - Critical Functionality] Defense-in-depth `sqlite3.OperationalError` guard in `lookup_symbols_exact`**
- **Found during:** Task 9
- **Issue:** The three FTS5 search functions (`search_sections`, `search_symbols`, `search_examples`) all catch `sqlite3.OperationalError` and return `[]`, but `lookup_symbols_exact` had no such guard. A corrupt index or a syntactically-valid-but-semantically-bad collation call could propagate as an unclassified `Internal error`.
- **Fix:** Added the same `try/except sqlite3.OperationalError` + `logger.warning` + `return []` pattern. Not strictly required by I-3 (which is specifically about case-insensitive matching), but protects the same symbol fast-path that the case-insensitive fix operates on.
- **Files modified:** `src/mcp_server_python_docs/retrieval/ranker.py`
- **Commit:** `a598756`

**3. [Rule 3 - Blocking] Task 1 sentinel pattern broke pyright**
- **Found during:** Task 12 verification gate
- **Issue:** The initial Task 1 implementation used `BeautifulSoup = None` / `Tag = None` / `md = None` on the ImportError path. This made pyright infer the types as `type[BeautifulSoup] | None` (Optional), which introduced 7 new pyright errors at every call site (`BeautifulSoup(body_html, "html.parser")`, `isinstance(sibling, Tag)`, etc.).
- **Fix:** Moved the real bs4/markdownify imports into a `TYPE_CHECKING` branch so static checkers always see the real types, and kept the runtime probe in a plain try/except that only flips the `_BUILD_DEPS_AVAILABLE` flag. Verified at runtime that import-time behavior still gracefully handles missing deps and `_ensure_build_deps()` still raises the actionable `ImportError`.
- **Files modified:** `src/mcp_server_python_docs/ingestion/sphinx_json.py`
- **Commit:** `23063d8`

**4. [Rule 1 - Lint Fix] Unused `pathlib.Path` import in `tests/test_detection.py`**
- **Found during:** Task 12 verification gate
- **Issue:** Ruff I001 — I imported `pathlib.Path` in the M-2 test file during iteration on the M-3 tests but never used it in the final version.
- **Fix:** Removed the import.
- **Files modified:** `tests/test_detection.py`
- **Commit:** `23063d8`

No other deviations. IN-01 (Windows `os.rename` in rollback) remains untouched per the explicit out-of-scope directive — verified via `git diff 7f9e84c..HEAD -- src/mcp_server_python_docs/ingestion/publish.py` that the `rollback()` function has no changes in any of the 12 commits.

## Final Verification Gate Results

Run after the 12th commit (`23063d8`):

- `uv run pytest -q` → **243 passed, 3 skipped** (baseline was 209 + 3 skipped; delta = +34 tests)
- `uv run ruff check .` → **All checks passed!**
- `uv run pyright` → **9 errors, 0 warnings** (all pre-existing in `tests/conftest.py`, `tests/test_services.py`, `tests/test_stdio_smoke.py`; zero new errors introduced by this plan)

## Test Count Delta

| Category | Baseline | After | Delta |
|----------|----------|-------|-------|
| Passing tests | 209 | 243 | +34 |
| Skipped tests | 3 | 3 | 0 |

New regression tests by finding:
- I-4 Task 1: +2 methods (`test_build_extras_present`, `test_build_deps_not_in_base`)
- M-2/M-3 Task 4+5: +17 (new `tests/test_detection.py`)
- M-5 Task 6: +6 methods (Mock-based gate tests)
- I-1 Task 8: +1 method (`test_get_docs_returns_empty_content_for_symbols_only_doc`)
- I-3 Task 9: +3 methods (case-insensitive lookup tests)
- M-4 Task 10: +3 methods (new `tests/test_server.py`)
- I-2 Task 11: +2 methods (WAL cleanup regression tests)

Total: +34 methods, matching the observed +34 in the suite count.

## Open Follow-ups

None. The plan scope is fully closed. Pre-existing pyright errors in `tests/conftest.py` (2), `tests/test_services.py` (6), and `tests/test_stdio_smoke.py` (1) remain as baseline project issues outside this plan's scope.

## Key Links

- `publish.py::publish_index` now calls `finalize_for_swap(conn)` from `storage/db.py` before `atomic_swap()` — single RW connection spans all three `ingestion_runs` updates.
- `server.py::create_server` tool shims all call `_require_ctx(ctx)` as their first line; `AppContext` no longer carries `detected_python_source` (dead field removed).
- `retrieval/query.py::classify_query` gates on `len(query) < 2` before `symbol_exists_fn(query)` is invoked; dotted queries continue to take the fast-path without any DB hit.

## Self-Check: PASSED

Verified by direct inspection:
- All 12 commits present in `git log --oneline 7f9e84c..HEAD`.
- `rollback()` function untouched in `src/mcp_server_python_docs/ingestion/publish.py` (IN-01 preserved).
- `grep -rn detected_python_source src tests` returns zero matches.
- `pyproject.toml` shows `beautifulsoup4` and `markdownify` only under `[project.optional-dependencies].build`.
- Runtime simulation confirmed `_ensure_build_deps()` raises actionable `ImportError` with the install hint when both deps are missing.

## Round 3 Ratifications

**I-3 (case-insensitive symbol lookup) — `COLLATE NOCASE` vs `normalized_name`.** Round 3 review flagged that the Task 9 fix in `retrieval/ranker.py::lookup_symbols_exact` uses `WHERE qualified_name = ? COLLATE NOCASE` rather than querying the already-populated `normalized_name` column. The two approaches are correctness-equivalent: `normalized_name` stores the lower-cased form of `qualified_name`, so `WHERE normalized_name = LOWER(?)` would return the same result set. Neither column has an index covering this exact-match scan today (`symbols` only indexes `(doc_set_id, qualified_name)`-style composites), so neither approach pays a planned-performance penalty over the other in v0.1.0 — the symbol fast-path is already gated on the query being a short dotted name, which keeps the scan row-count tiny in practice. Adding `CREATE INDEX idx_symbols_normalized ON symbols(normalized_name)` plus switching the query to `LOWER(?)` is a clean v1.1 change once real symbol-lookup query volume is measured; the schema already reserves the column, so there's no migration cost. **Disposition:** ratified as-is for v0.1.0, deferred to v1.1.
Loading
Loading