# Plan Review

Reviewed against `docs/plans/2026-02-19-memex-implementation.md`. Tasks 1-3 are committed and passing on `feature/implement-memex`. Tasks 4-9 have not been implemented — this review covers the full plan.

---

## Task 4: File Indexer — Heading stack produces wrong output

Severity: **test failure**

The `chunk_markdown` algorithm nests headings hierarchically (h2 under h1, h3 under h2), but the test assertions expect a flat structure where h2 headings are top-level.

The pop condition `while heading_levels and heading_levels[-1] >= level` never removes an h1 when an h2 arrives, because `1 >= 2` is False. So "Overview" (h1) stays on the stack when "Architecture" (h2) is processed.

Algorithm output vs test expectations for `test_chunk_markdown_splits_by_headings`:

```
Algorithm produces:                        Test expects:
README.md > Overview                       README.md > Overview
README.md > Overview > Architecture        README.md > Architecture
README.md > Overview > Architecture > …    README.md > Architecture > Backend
README.md > Overview > Frontend            README.md > Frontend
```

Fix options:

1. Change the algorithm to clear the entire stack on each new heading, then only re-push ancestors. This gives the flat structure the tests expect.
2. Change the tests to expect hierarchical nesting (`README.md > Overview > Architecture`), which is what the algorithm actually does.

Option 2 is arguably more useful for memory retrieval (more context in the heading path), but either way the plan needs to pick one and be consistent.

---

## Task 5: File Watcher — Root-level files silently skipped

Severity: **test failure**

The default watch patterns use `**/*.md`, `**/*.yaml`, etc. Python's `fnmatch.fnmatch` does not treat `/` as special, but the pattern `**/*.md` still structurally requires a `/` between the `**` and `*.md` portions. A root-level file like `README.md` has relative path `"README.md"` (no `/`), so it doesn't match.

Verified:

```python
>>> fnmatch.fnmatch("README.md", "**/*.md")
False
>>> fnmatch.fnmatch("docs/README.md", "**/*.md")
True
```

This means `test_reconcile_indexes_new_files` will fail — it creates `test.md` at the watch root and expects `store.count() == 1`, but the file won't match `["**/*.md"]`.

More importantly, the files you most want indexed (`CLAUDE.md`, `README.md`, `pyproject.toml` at project root) would be silently ignored in real usage.

Fix options:

1. Add root-level patterns alongside the recursive ones: `["*.md", "**/*.md", "*.yaml", "**/*.yaml", ...]`
2. Switch from `fnmatch` to `pathlib.PurePath.match()` or `wcmatch.fnmatch` which handle `**` as recursive glob properly.
3. Special-case root-level files in `matches_patterns` (check basename against `*.ext` when there's no `/`).

Option 1 is the simplest. Option 2 is the most correct.

---

## Task 5: File Watcher — `rstrip("/**")` is character-based, not substring-based

Severity: **latent bug** (doesn't cause failures with current patterns)

```python
if fnmatch.fnmatch(partial, exclude.rstrip("/**")):
```

`str.rstrip("/**")` strips individual characters from the set `{"/", "*"}`, not the literal substring `"/**"`. For `"node_modules/**"` this coincidentally produces `"node_modules"`, but the behavior is wrong in general:

```
"node_modules/**".rstrip("/**")  → "node_modules"     # correct by accident
"logs*/**".rstrip("/**")         → "logs"              # wrong, should be "logs*"
"**/cache".rstrip("/**")         → "cache"             # wrong, strips from wrong end
```

Fix: use `removesuffix("/**")` (Python 3.9+, and the project requires 3.12).

---

## Task 6: MCP Server — Watcher threads never stopped

Severity: **minor** (no correctness issue, but unclean)

`get_store()` calls `_watcher.start()` to launch watchdog observer threads, but there is no corresponding shutdown path. When the MCP server process exits, the threads are orphaned. For a stdio transport server this is mostly harmless (process exit kills everything), but it would matter if `create_server` were used in tests or embedded contexts.

Fix: register an `atexit` handler or add a shutdown hook that calls `_watcher.stop()`.

---

## Task 6: MCP Server — `index_files` tool creates redundant watcher

Severity: **cosmetic**

The `index_files` tool instantiates a new `FileWatcher` on every call instead of reusing the `_watcher` created by `get_store()`. Reconciliation is idempotent so this isn't incorrect, just unnecessary object creation.

---

## Task 7: Integration test inherits the root-level file bug

Severity: **test failure** (cascade from Task 5)

The integration test creates `CLAUDE.md` and `AGENT.md` directly inside `project_dir`, then calls `watcher.reconcile()` and asserts `file_count == 2`. Since `add_project` uses the default patterns (`["**/*.md", ...]`) and these root-level files don't match via fnmatch, reconcile will find 0 files. The assertion `file_count == 2` fails, and every subsequent assertion (searching for "npm test", checking topics) also fails because nothing was indexed.

This is entirely downstream of the Task 5 root-level file bug. Fixing Task 5 fixes this.

---

## Task 7: Unused imports in integration test

Severity: **cosmetic**

`test_integration.py` imports `yaml` and `index_file` but uses neither. The test uses `watcher.reconcile()` for indexing and never touches YAML directly.

---

## Task 9: Tilde in YAML config not expanded by the loader

Severity: **runtime failure**

The manual config example sets `data_dir: ~/.memex/data`. YAML loads this as the literal string `"~/.memex/data"`. The config loader does `raw.get("data_dir", DEFAULT_DATA_DIR)` and passes the result straight through — no `os.path.expanduser()` call. `DEFAULT_DATA_DIR` is correctly expanded (it's set at module load time via `os.path.expanduser`), but that default is only used when the key is absent from the config file.

When the key IS present, ChromaDB receives `"~/.memex/data"` as a literal path, which would create a directory called `~` in the current working directory instead of the user's home.

Fix: add `os.path.expanduser()` in `MemexConfig.load()` when reading `data_dir` from YAML, or in the `MemoryStore` constructor. One line.

---

## Task 3 (already committed): Dead variable in `store.py`

Severity: **cosmetic**

`store.py` search method sets `where = None` but never uses it. Leftover from what was likely planned as a ChromaDB `where` filter before the post-query tag filtering approach was chosen.

---

## Summary

| Task | Issue | Severity | Blocks? |
|------|-------|----------|---------|
| 4 | Heading stack vs test mismatch | test failure | yes |
| 5 | Root-level files don't match `**/*.md` | test failure | yes |
| 5 | `rstrip("/**")` not substring removal | latent bug | no |
| 6 | Watcher threads never stopped | minor | no |
| 6 | `index_files` creates redundant watcher | cosmetic | no |
| 7 | Integration test inherits root-level file bug | test failure | yes (cascade) |
| 7 | Unused imports (`yaml`, `index_file`) | cosmetic | no |
| 9 | Tilde in YAML config not expanded | runtime failure | yes |
| 3 | Dead `where = None` variable | cosmetic | no |

Four issues will cause failures: the heading stack mismatch (Task 4), root-level file matching (Task 5, cascading into Task 7), and tilde expansion (Task 9). The rest are minor or cosmetic. The plan needs amendments on these four before implementation should proceed.
