# Python 3.14 Floor + Remove Markitdown Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Raise Nexus's Python floor to 3.14 everywhere (pyprojects, CI, Dockerfiles, templates, docs) and remove `markitdown` from the repo entirely, leaving `pdf-inspector` as the sole local parser.

**Architecture:** Single PR that (a) bumps `requires-python = ">=3.14"` across every `pyproject.toml`, (b) deletes `markitdown_parser.py`, `markitdown_provider.py`, and the `nexus[markitdown]` optional-dependencies extra, (c) unpins `pdf-inspector` (removes the `python_version == '3.12'` marker), (d) deletes the `compression.zstd` fallback code path and `[compression]` extra since 3.14 has stdlib zstd, (e) updates every CI workflow and Dockerfile to Python 3.14, and (f) purges all markitdown references from source, tests, and docs. Existing pdf-inspector tests provide coverage; no new tests are added.

**Tech Stack:** Python 3.14, uv, mypy, ruff, pytest, GitHub Actions, Docker.

**Design Spec:** `docs/superpowers/specs/2026-04-16-python-3.14-floor-drop-markitdown-design.md`

---

## File Structure

### Files deleted

- `src/nexus/bricks/parsers/markitdown_parser.py`
- `src/nexus/bricks/parsers/providers/markitdown_provider.py`

### Files modified

**Pyprojects (all 5):**
- `pyproject.toml` (root)
- `packages/nexus-fs/pyproject.toml`
- `rust/kernel/pyproject.toml`
- `rust/raft/pyproject.toml`
- `rust/tasks/pyproject.toml`

**Source (parsers module):**
- `src/nexus/bricks/parsers/__init__.py`
- `src/nexus/bricks/parsers/brick.py`
- `src/nexus/bricks/parsers/utils.py`
- `src/nexus/bricks/parsers/pdf_inspector_parser.py`
- `src/nexus/bricks/parsers/providers/__init__.py`
- `src/nexus/bricks/parsers/providers/base.py`
- `src/nexus/bricks/parsers/providers/registry.py`

**Source (config + views):**
- `src/nexus/config.py`
- `src/nexus/core/config.py`
- `src/nexus/lib/virtual_views.py`

**Source (compression):**
- `src/nexus/backends/wrappers/compressed.py`

**Tests:**
- `tests/unit/cli/test_config.py`
- `tests/unit/core/test_exceptions.py`
- `tests/unit/core/test_base_registry.py`
- `tests/unit/bricks/parsers/test_pdf_inspector_parser.py`
- `tests/unit/bricks/parsers/providers/test_provider_registry.py`

**CI (all `.github/workflows/*.yml`):**
- `test.yml`, `lint.yml`, `benchmark.yml`, `code-quality.yml`, `api-surface.yml`, `pyinstaller-cluster.yml`, `release.yml`, `release-nexus-fs.yml`, `docs-nexus-fs.yml`, `conda-pack.yml`
- `.github/actions/build-rust-extensions/action.yml`

**Dockerfiles:**
- `Dockerfile`
- `dockerfiles/Dockerfile.remote`
- `dockerfiles/Dockerfile.minimal`
- `tests/e2e/self_contained/Dockerfile.fuse-test`
- (`dockerfiles/Dockerfile.pypi` already on 3.14 — no change)

**Templates + docs:**
- `.github/ISSUE_TEMPLATE/question.md`
- `.github/ISSUE_TEMPLATE/performance.md`
- `.github/ISSUE_TEMPLATE/bug_report.md`
- `.github/DISCUSSION_TEMPLATE/q-and-a.yml`
- `README.md`
- `docs/guides/user-guide.md`

**Lockfile:**
- `uv.lock` (regenerated)

---

## Task 1: Verify 3.14 baseline (no code changes)

Goal: confirm the current branch installs and tests pass on Python 3.14 before any edits, so any later failures are attributable to our changes.

**Files:**
- None (environment setup only)

- [ ] **Step 1: Create Python 3.14 venv**

```bash
python3.14 -m venv .venv-314
source .venv-314/bin/activate
python -V
```

Expected: `Python 3.14.x`.

- [ ] **Step 2: Install with forward-compat env var**

```bash
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv sync --python .venv-314/bin/python
```

Expected: clean install, no resolver errors. `pdf-inspector` builds from sdist.

- [ ] **Step 3: Run the parser unit tests to establish a green baseline**

```bash
.venv-314/bin/python -m pytest tests/unit/bricks/parsers/ -v
```

Expected: all tests pass on 3.14. If any fail before our edits, stop and investigate — we need a clean starting state.

- [ ] **Step 4: No commit — baseline only**

---

## Task 2: Bump `requires-python` + classifiers in root pyproject

**Files:**
- Modify: `pyproject.toml`

- [ ] **Step 1: Change `requires-python`**

Edit `pyproject.toml:7`:

```toml
# Before:
requires-python = ">=3.12"

# After:
requires-python = ">=3.14"
```

- [ ] **Step 2: Prune classifiers**

Edit `pyproject.toml:32-34`. Remove the 3.12 and 3.13 lines, keep 3.14:

```toml
# Before:
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",

# After:
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.14",
```

- [ ] **Step 3: Verify `uv sync` still resolves on 3.14**

```bash
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv sync --python .venv-314/bin/python
```

Expected: clean resolution.

- [ ] **Step 4: Commit**

```bash
git add pyproject.toml
git commit -m "build: bump Python floor to 3.14 in root pyproject"
```

---

## Task 3: Bump `requires-python` + classifiers in sibling pyprojects

**Files:**
- Modify: `packages/nexus-fs/pyproject.toml`
- Modify: `rust/kernel/pyproject.toml`
- Modify: `rust/raft/pyproject.toml`
- Modify: `rust/tasks/pyproject.toml`

- [ ] **Step 1: Update `packages/nexus-fs/pyproject.toml`**

Change the `requires-python` line to `">=3.14"`. Remove 3.12/3.13 classifier lines (lines 22-23 per current tree), keeping the 3.14 line.

- [ ] **Step 2: Update each of `rust/kernel/pyproject.toml`, `rust/raft/pyproject.toml`, `rust/tasks/pyproject.toml`**

Change the `requires-python` line (line 9 in each):

```toml
# Before:
requires-python = ">=3.12"

# After:
requires-python = ">=3.14"
```

(The Rust pyprojects have no Python classifiers section; `requires-python` is the only change.)

- [ ] **Step 3: Confirm no stray references**

```bash
grep -rn 'requires-python = ">=3.1[23]"' .
```

Expected: no matches.

- [ ] **Step 4: Commit**

```bash
git add packages/nexus-fs/pyproject.toml rust/kernel/pyproject.toml rust/raft/pyproject.toml rust/tasks/pyproject.toml
git commit -m "build: bump Python floor to 3.14 in sibling pyprojects"
```

---

## Task 4: Bump mypy `python_version` to 3.14

**Files:**
- Modify: `pyproject.toml` (root, line 427)
- Modify: `packages/nexus-fs/pyproject.toml` (line 126 in current tree)

- [ ] **Step 1: Update root mypy config**

Edit `pyproject.toml:427`:

```toml
# Before:
python_version = "3.11"

# After:
python_version = "3.14"
```

- [ ] **Step 2: Update the stale proto comment**

Edit `pyproject.toml:468` (inside `[[tool.mypy.overrides]]` for `nexus.storage._metadata_mapper_generated`):

```toml
# Before:
# Proto-generated metadata_pb2 resolves to Any on Python 3.13 mypy

# After:
# Proto-generated metadata_pb2 resolves to Any without a generated stub
```

- [ ] **Step 3: Update `packages/nexus-fs/pyproject.toml` mypy**

Same change: `python_version = "3.11"` → `"3.14"`.

- [ ] **Step 4: Run mypy to confirm no regressions from the version bump**

```bash
.venv-314/bin/python -m mypy src/
```

Expected: existing error count or lower (not higher). Record baseline count if unfamiliar.

- [ ] **Step 5: Commit**

```bash
git add pyproject.toml packages/nexus-fs/pyproject.toml
git commit -m "build: set mypy python_version to 3.14"
```

---

## Task 5: Remove conditional pdf-inspector/markitdown pins; drop core markitdown dep

**Files:**
- Modify: `pyproject.toml` (dependencies block, around lines 93-114)

- [ ] **Step 1: Replace the entire parser-matrix comment block + conditional pins with a single unconditional `pdf-inspector` dep**

Find the block spanning roughly lines 93-114 in `pyproject.toml`:

```toml
    # PDF parsing.  Two parsers with different Python-version support matrices
    # — at least one must install cleanly on every supported interpreter so
    # PDFs still reach the search index without a manual setup step.
    #
    #   * pdf-inspector (preferred local provider; ~2-3 MB Rust ext, zero
    #     Python deps).  Upstream 0.1.1 only ships cp312 wheels; on 3.13/3.14
    #     pip falls back to sdist and needs Rust + PYO3_USE_ABI3_FORWARD_-
    #     COMPATIBILITY=1, neither available in normal installs.  Docker
    #     sets the env var and still installs it across 3.12-3.14.
    #   * markitdown (fallback, ~200 MB transitive deps).  Ships wheels on
    #     3.12-3.13; the 3.14 sdist is not yet compatible.  The parser
    #     registry picks whichever is available at priority order, so on
    #     3.13 markitdown keeps PDFs indexable even though pdf-inspector
    #     is absent.
    #
    # Python 3.14 users currently have neither parser by default.  Install
    # the ``markitdown`` extra or set up pdf-inspector with Rust until
    # upstream ships 3.14 wheels.
    "pdf-inspector>=0.1.1; python_version == '3.12'",
    "markitdown[all]>=0.1.0; python_version == '3.13'",
```

Replace with:

```toml
    # Default local PDF parser (Rust + PyO3, ~2-3 MB, zero Python deps).
    # On Python 3.14 without a cp314 wheel, pip builds from sdist and needs
    # Rust plus ``PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`` (both present in
    # the Nexus Docker image and typical dev setups).
    "pdf-inspector>=0.1.1",
```

- [ ] **Step 2: Confirm no remaining conditional Python-version markers in the core `dependencies` list**

```bash
grep -n "python_version" pyproject.toml
```

Expected: matches only in `[project.optional-dependencies].markitdown` (next task deletes it) and `[project.optional-dependencies].compression` (deleted in Task 8). No matches in the core `dependencies = [ ... ]` array.

- [ ] **Step 3: Commit**

```bash
git add pyproject.toml
git commit -m "build(deps): drop markitdown core dep, unpin pdf-inspector"
```

---

## Task 6: Delete the `[markitdown]` optional-dependencies extra

**Files:**
- Modify: `pyproject.toml` (lines 157-163 in current tree)

- [ ] **Step 1: Delete the `markitdown` extra block**

Remove this block from `pyproject.toml`:

```toml
markitdown = [
    # Office/HTML/EPUB/audio parsing fallback. ~200 MB transitive deps
    # (pandas, numpy, lxml, onnxruntime, pdfminer, python-pptx, mammoth, openpyxl, ...).
    # Install with: pip install nexus[markitdown]
    "markitdown[all]>=0.1.0; python_version < '3.14'",
]

```

The `[project.optional-dependencies]` header stays; the `markitdown = [...]` group under it is deleted. The next extra in the file (`performance = [...]` at line 165 in the current tree) becomes the first entry.

- [ ] **Step 2: Verify no stray markitdown mentions in optional-dependencies**

```bash
grep -n "markitdown" pyproject.toml
```

Expected: no matches anywhere in `pyproject.toml`.

- [ ] **Step 3: Commit**

```bash
git add pyproject.toml
git commit -m "build(deps): delete nexus[markitdown] optional extra"
```

---

## Task 7: Delete markitdown source files and all imports/registrations

**Files:**
- Delete: `src/nexus/bricks/parsers/markitdown_parser.py`
- Delete: `src/nexus/bricks/parsers/providers/markitdown_provider.py`
- Modify: `src/nexus/bricks/parsers/__init__.py`
- Modify: `src/nexus/bricks/parsers/brick.py`
- Modify: `src/nexus/bricks/parsers/providers/__init__.py`
- Modify: `src/nexus/bricks/parsers/providers/registry.py`

- [ ] **Step 1: Delete the two files**

```bash
git rm src/nexus/bricks/parsers/markitdown_parser.py
git rm src/nexus/bricks/parsers/providers/markitdown_provider.py
```

- [ ] **Step 2: Update `src/nexus/bricks/parsers/__init__.py`**

Remove the import at line 27 and the `"MarkItDownParser",` entry in `__all__` at line 52. The docstring at line 33 mentions `MarkItDownParser` — replace the phrase with `PdfInspectorParser`. Check line 52 for the full `__all__` tuple and drop the entry cleanly (no dangling comma issues).

Specifically, these lines should go away:

```python
from nexus.bricks.parsers.markitdown_parser import MarkItDownParser
```

and inside `__all__`:

```python
    "MarkItDownParser",
```

The `create_parse_fn` docstring currently says "Create a parse callback using MarkItDownParser." Update to "Create a parse callback using PdfInspectorParser."

- [ ] **Step 3: Update `src/nexus/bricks/parsers/brick.py`**

Remove line 16:

```python
from nexus.bricks.parsers.markitdown_parser import MarkItDownParser
```

Remove line 56 (or whichever line registers the parser):

```python
self._parser_registry.register(MarkItDownParser())
```

Also strip two comments/docstring references:
- Line 32 (roughly): `- Shares a single ``MarkItDownParser`` across registries` → delete this bullet; replace with a line describing the pdf-inspector-only setup.
- Line 53 (roughly): `# MarkItDownParser covers non-PDF formats (when markitdown is installed).` → delete.
- Line 98 (roughly): `Uses the shared ``ParserRegistry`` (no redundant MarkItDown instance).` → change to `Uses the shared ``ParserRegistry``.` (drop the trailing qualifier).

- [ ] **Step 4: Update `src/nexus/bricks/parsers/providers/__init__.py`**

Remove the `MarkItDownProvider` re-export (line 8 in current tree notes it in the module docstring — remove that bullet too; actual import/export must be stripped).

- [ ] **Step 5: Update `src/nexus/bricks/parsers/providers/registry.py`**

Delete the markitdown registration block (lines 266-276 in current tree):

```python
        # Try to register MarkItDown (optional fallback for non-PDF formats)
        try:
            from nexus.bricks.parsers.providers.markitdown_provider import MarkItDownProvider

            config = config_map.get("markitdown", ProviderConfig(name="markitdown", priority=10))
            markitdown_provider = MarkItDownProvider(config)
            if markitdown_provider.is_available():
                self.register(markitdown_provider)
                registered += 1
        except Exception as e:
            logger.debug("MarkItDown provider not available: %s", e)

```

After this deletion, the `logger.info("Auto-discovered %d parse providers", registered)` line becomes the next non-blank line after the pdf-inspector block (ensure exactly one blank line separator).

- [ ] **Step 6: Run the parser unit tests**

```bash
.venv-314/bin/python -m pytest tests/unit/bricks/parsers/ -v
```

Expected: one or two tests fail with `ImportError` or `AssertionError` pointing at `MarkItDownParser` / `MarkItDownProvider`. These are addressed in Task 10 (test edits). Do not fix them here — the failing tests are the next task's entry point.

- [ ] **Step 7: Commit**

```bash
git add src/nexus/bricks/parsers/
git commit -m "refactor(parsers): delete markitdown parser and provider"
```

---

## Task 8: Strip markitdown docstring/comment references across the codebase

**Files:**
- Modify: `src/nexus/bricks/parsers/utils.py`
- Modify: `src/nexus/bricks/parsers/pdf_inspector_parser.py`
- Modify: `src/nexus/bricks/parsers/providers/base.py`
- Modify: `src/nexus/config.py`
- Modify: `src/nexus/core/config.py`
- Modify: `src/nexus/lib/virtual_views.py`

- [ ] **Step 1: `src/nexus/bricks/parsers/utils.py:3`**

```python
# Before:
"""Deduplicated from MarkItDownParser, MarkItDownProvider, and LlamaParseProvider"""

# After:
"""Shared parser helpers used by PdfInspectorProvider and LlamaParseProvider."""
```

- [ ] **Step 2: `src/nexus/bricks/parsers/pdf_inspector_parser.py:38`**

Replace the priority comment:

```python
# Before:
        # Outrank MarkItDownParser (default 0) for .pdf

# After:
        # Higher than the Parser base default (0) so pdf-inspector is picked first for .pdf
```

- [ ] **Step 3: `src/nexus/bricks/parsers/providers/base.py:15,38`**

Replace the docstring hints:

```python
# Line 15 before:
#   name: Provider name (e.g., "unstructured", "llamaparse", "markitdown")
# Line 15 after:
#   name: Provider name (e.g., "unstructured", "llamaparse", "pdf-inspector")

# Line 38 before:
#   Providers can be local (MarkItDown) or API-based (Unstructured, LlamaParse).
# Line 38 after:
#   Providers can be local (pdf-inspector) or API-based (Unstructured, LlamaParse).
```

- [ ] **Step 4: `src/nexus/config.py`**

Three changes:

Line 199 comment (roughly):

```python
# Before:
# Supports multiple providers: unstructured, llamaparse, pdf-inspector, markitdown

# After:
# Supports multiple providers: unstructured, llamaparse, pdf-inspector
```

Line 207 docstring/message:

```python
# Before:
"Providers: unstructured (API), llamaparse (API), pdf-inspector (local PDF, default), markitdown (local fallback for non-PDF, optional)"

# After:
"Providers: unstructured (API), llamaparse (API), pdf-inspector (local PDF, default)"
```

Lines 612-616 — delete the entire markitdown env-probe block:

```python
# Before:
    # markitdown: optional fallback for non-PDF formats (Office/HTML/EPUB/...)
    try:
        from markitdown import MarkItDown  # noqa: F401

        parse_providers.append({"name": "markitdown", "priority": 10})
    except ImportError:
        pass
```

(Delete the 6-line block; leave the surrounding env-loader logic untouched.)

- [ ] **Step 5: `src/nexus/core/config.py:97`**

```python
# Before:
# document parsing (unstructured, llamaparse, markitdown).

# After:
# document parsing (unstructured, llamaparse, pdf-inspector).
```

- [ ] **Step 6: `src/nexus/lib/virtual_views.py:9`**

```python
# Before:
3. Parses it using the appropriate parser (MarkItDown)

# After:
3. Parses it using the appropriate parser (pdf-inspector)
```

- [ ] **Step 7: Sanity grep**

```bash
grep -rn -i "markitdown" src/
```

Expected: zero matches.

- [ ] **Step 8: Commit**

```bash
git add src/
git commit -m "refactor: purge markitdown references from source docstrings and config"
```

---

## Task 9: Remove `compression.zstd` fallback code and `[compression]` extra

**Files:**
- Modify: `src/nexus/backends/wrappers/compressed.py`
- Modify: `pyproject.toml` (`[project.optional-dependencies].compression` block, lines 179-183 in current tree)

- [ ] **Step 1: Simplify the zstd availability path in `compressed.py`**

Replace the try/except ladder at lines 55-87 (roughly):

```python
# zstd availability detection (Python 3.14+ stdlib or zstandard package)
# ---------------------------------------------------------------------------

_ZSTD_AVAILABLE = False
_zstd_compress: object | None = None
_zstd_decompress: object | None = None

try:
    from compression import zstd  # Python 3.14+

    _zstd_compress = zstd.compress
    _zstd_decompress = zstd.decompress
    _ZSTD_AVAILABLE = True
    logger.debug("[COMPRESSED_WRAPPER] Using Python 3.14+ native zstd")
except ImportError:
    try:
        import zstandard

        def _compat_compress(data: bytes, *, level: int = 3) -> bytes:
            compressor = zstandard.ZstdCompressor(level=level)
            return bytes(compressor.compress(data))

        def _compat_decompress(data: bytes) -> bytes:
            decompressor = zstandard.ZstdDecompressor()
            return bytes(decompressor.decompress(data))

        _zstd_compress = _compat_compress
        _zstd_decompress = _compat_decompress
        _ZSTD_AVAILABLE = True
        logger.debug("[COMPRESSED_WRAPPER] Using zstandard package")
    except ImportError:
        logger.debug("[COMPRESSED_WRAPPER] zstd not available")
```

With the 3.14-only path:

```python
# zstd availability detection (Python 3.14+ stdlib)
# ---------------------------------------------------------------------------

from compression import zstd

_zstd_compress = zstd.compress
_zstd_decompress = zstd.decompress
_ZSTD_AVAILABLE = True
```

The `_ZSTD_AVAILABLE` flag, `_zstd_compress`, and `_zstd_decompress` module-level names are preserved so downstream code keeps working.

- [ ] **Step 2: Simplify the availability runtime check**

Edit `compressed.py:154-158` (roughly — inside `CompressedBackend.__init__`):

```python
# Before:
        if not _ZSTD_AVAILABLE:
            raise RuntimeError(
                "zstd compression not available. Install the 'zstandard' package "
                "or upgrade to Python 3.14+ for native compression.zstd support."
            )
```

On 3.14-only the flag is always True — remove the check entirely (delete the 5-line `if not _ZSTD_AVAILABLE: raise ...` block).

- [ ] **Step 3: Replace the cached compressor path**

Edit `compressed.py:172-182` (roughly):

```python
# Before:
        # Cache compressor instance for reuse.
        # ZstdCompressor.compress() is documented thread-safe (stateless per call).
        # See: https://github.com/indygreg/python-zstandard/blob/main/README.rst
        try:
            import zstandard

            self._cached_compressor: zstandard.ZstdCompressor | None = zstandard.ZstdCompressor(
                level=self._config.level
            )
        except ImportError:
            self._cached_compressor = None

# After:
        # Cache compressor instance for reuse.
        from compression.zstd import ZstdCompressor

        self._cached_compressor: ZstdCompressor = ZstdCompressor(level=self._config.level)
```

Note: stdlib `compression.zstd.ZstdCompressor.compress(data)` returns `bytes` directly, matching the usage downstream. If call-sites pass additional kwargs, keep the signature aligned with the previous `zstandard.ZstdCompressor` API. Grep for `self._cached_compressor` to confirm all call sites still type-check.

- [ ] **Step 4: Search for any other `zstandard`-specific references**

```bash
grep -rn "zstandard" src/ tests/
```

Expected: zero matches after the edits above. If references remain (e.g. tests that inject a mock), update them to use `compression.zstd` types.

- [ ] **Step 5: Delete the `[compression]` optional extra in `pyproject.toml`**

Remove the block at lines 179-183 in the current tree:

```toml
compression = [
    # zstd compression for CompressedStorage wrapper (Issue #1705)
    # Python 3.14+ has native compression.zstd; this is the fallback for 3.12/3.13
    "zstandard>=0.22.0",
]
```

If `compression` is the only thing between other extras, also clean up an extra blank line if needed.

- [ ] **Step 6: Run the affected tests**

```bash
.venv-314/bin/python -m pytest tests/unit/backends/ -v -k compressed
```

Expected: green. If a test mocks `zstandard`, update it to use stdlib names inline.

- [ ] **Step 7: Commit**

```bash
git add src/nexus/backends/wrappers/compressed.py pyproject.toml
git commit -m "refactor(compression): use stdlib compression.zstd on 3.14+; drop zstandard fallback"
```

---

## Task 10: Update tests (delete/rename/edit the markitdown references)

**Files:**
- Modify: `tests/unit/cli/test_config.py`
- Modify: `tests/unit/core/test_exceptions.py`
- Modify: `tests/unit/core/test_base_registry.py`
- Modify: `tests/unit/bricks/parsers/test_pdf_inspector_parser.py`
- Modify: `tests/unit/bricks/parsers/providers/test_provider_registry.py`

- [ ] **Step 1: `tests/unit/cli/test_config.py` — delete the markitdown-specific test**

Delete the function `test_load_from_environment_skips_markitdown_when_unavailable` starting at line 442. Also delete any surrounding decorator / blank line. Grep to confirm no helper imports become unused afterward; if `sys` was imported only for this test, drop the import too.

- [ ] **Step 2: `tests/unit/core/test_exceptions.py` — replace `"MarkItDown"` string in `ParserError` tests**

Lines 129-137 (roughly) construct `ParserError` with `parser="MarkItDown"`. Replace the string literal in each construction with `"PdfInspector"`. The assertions that check `"[MarkItDown]"` should become `"[PdfInspector]"`. The test still validates that `ParserError` formats the `parser` argument into `str(error)`.

Example:

```python
# Before:
    error = ParserError("Parsing failed", parser="MarkItDown")
    assert "[MarkItDown]" in str(error)
    assert error.parser == "MarkItDown"

# After:
    error = ParserError("Parsing failed", parser="PdfInspector")
    assert "[PdfInspector]" in str(error)
    assert error.parser == "PdfInspector"
```

Apply the same substitution to the second `ParserError` construction a few lines below.

- [ ] **Step 3: `tests/unit/core/test_base_registry.py:158` — update the inline comment**

```python
# Before:
        assert count >= 1  # at least MarkItDownParser

# After:
        assert count >= 1  # at least PdfInspectorParser
```

- [ ] **Step 4: `tests/unit/bricks/parsers/test_pdf_inspector_parser.py` — strip markitdown framing**

Line 21 comment:

```python
# Before:
    # MarkItDownParser returns default 0 from Parser.priority

# After:
    # Parser base class returns default priority 0
```

Lines 81-91 test function:

```python
# Before:
# pdf-inspector on a default install (markitdown not installed). This was
# the regression guard for the issue that prevented PDFs from parsing when
# MarkItDownParser.can_parse returned False.
def test_brick_create_parse_fn_handles_pdf_without_markitdown():

# After:
# pdf-inspector on a default install.  Confirms the brick wires pdf-inspector
# into the auto-parse-on-write pipeline (``create_parse_fn`` /
# ``AutoParseWriteHook``) so PDFs flow to the indexer.
def test_brick_create_parse_fn_handles_pdf():
```

Update any call sites of the renamed test (grep for the old name to confirm it's only defined once).

- [ ] **Step 5: `tests/unit/bricks/parsers/providers/test_provider_registry.py:40` — rename and retarget**

Rename `test_auto_discover_pdf_inspector_outranks_markitdown_for_pdf` to `test_auto_discover_pdf_inspector_registered_for_pdf`. Trim the test body: keep the part asserting pdf-inspector gets registered at priority 20 for `.pdf`; delete any setup/asserts that mock or reference `MarkItDownProvider`.

Expected final shape:

```python
def test_auto_discover_pdf_inspector_registered_for_pdf(monkeypatch):
    # existing monkeypatch + auto_discover setup (minus markitdown mocks)
    # existing assert that pdf-inspector is registered at priority 20
    # existing assert that get_provider(".pdf") returns the pdf-inspector provider
```

- [ ] **Step 6: Run the full unit suite**

```bash
.venv-314/bin/python -m pytest tests/unit/ -v
```

Expected: all tests pass. If failures surface outside the files above, they are likely from Task 7/8 docstring drift — investigate and fix.

- [ ] **Step 7: Commit**

```bash
git add tests/
git commit -m "test: drop markitdown-specific tests; retarget remaining assertions to pdf-inspector"
```

---

## Task 11: Regenerate `uv.lock`

**Files:**
- Modify: `uv.lock`

- [ ] **Step 1: Full relock on Python 3.14**

```bash
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv lock
```

Expected: lockfile regenerates. The markitdown subtree (pandas, numpy pinned variants not required by other deps, onnxruntime, magika, pdfminer, mammoth, openpyxl, python-pptx, lxml pinned variants) drops out. `zstandard` drops out.

- [ ] **Step 2: Inspect the diff for unexpected bumps**

```bash
git diff --stat uv.lock
git diff uv.lock | head -200
```

Review: lines removed should dominate. Any added/bumped deps that are NOT markitdown/zstandard children should be scrutinized. If a non-parser package jumped a major version, stop and consider narrowing with `uv lock --upgrade-package <name>=<old>` or splitting the bump to a separate PR.

- [ ] **Step 3: Validate the lockfile installs**

```bash
rm -rf .venv-314
python3.14 -m venv .venv-314
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv sync --python .venv-314/bin/python --frozen
```

Expected: clean install from the regenerated lockfile.

- [ ] **Step 4: Run the full test suite once more**

```bash
.venv-314/bin/python -m pytest
```

Expected: green.

- [ ] **Step 5: Commit**

```bash
git add uv.lock
git commit -m "build(deps): regenerate uv.lock for 3.14 floor + markitdown removal"
```

---

## Task 12: Update CI workflows to Python 3.14

**Files (all in `.github/workflows/`):**
- `test.yml`
- `lint.yml`
- `benchmark.yml`
- `code-quality.yml`
- `api-surface.yml`
- `pyinstaller-cluster.yml`
- `release.yml`
- `release-nexus-fs.yml`
- `docs-nexus-fs.yml`
- `conda-pack.yml`
- `.github/actions/build-rust-extensions/action.yml`

- [ ] **Step 1: `.github/workflows/test.yml`**

Apply these changes:

```yaml
# Line ~49:
#   python-version: "3.13"
# becomes:
#   python-version: "3.14"

# Line ~58:
#   uv venv --python 3.13
# becomes:
#   uv venv --python 3.14

# Line ~95-98:
#   - name: Set up Python 3.13
#     python-version: "3.13"
# becomes:
#   - name: Set up Python 3.14
#     python-version: "3.14"

# Line ~124 (matrix):
#   python-version: ["3.13"]
# becomes:
#   python-version: ["3.14"]
```

Grep after editing: `grep -n '3\.1[23]' .github/workflows/test.yml` → expected: no matches.

- [ ] **Step 2: `.github/workflows/lint.yml`**

Change `python-version: "3.13"` (line ~26) → `"3.14"`. Change `uv venv --python 3.13` (line ~35) → `3.14`. Change `python-version: '3.12'` (line ~82) → `'3.14'`.

- [ ] **Step 3: `.github/workflows/benchmark.yml`**

Change both "Set up Python 3.13" blocks (lines ~26-29 and ~111-114) to `3.14`. Change both `uv venv --python 3.13` lines (lines ~38 and ~123) to `3.14`.

- [ ] **Step 4: `.github/workflows/code-quality.yml`**

Every `python-version: '3.12'` → `'3.14'` (approximately lines 70, 103, 144, 183, 205, 236 in the current tree).

- [ ] **Step 5: `.github/workflows/api-surface.yml`**

`python-version: '3.12'` (line ~29) → `'3.14'`.

- [ ] **Step 6: `.github/workflows/pyinstaller-cluster.yml`**

`python-version: "3.12"` (line ~90) → `"3.14"`.

- [ ] **Step 7: `.github/workflows/release.yml`**

Multiple changes:

- Line ~60: `python-version: "3.13"` → `"3.14"`.
- Line ~88: `python-version: "3.13"` → `"3.14"`.
- Line ~104: job name `py${{ matrix.python-version }}` — keep the template, but change matrix (below).
- Line ~110: `python-version: ["3.12", "3.13", "3.14"]` → `python-version: ["3.14"]`.
- Line ~155: `python-version: "3.13"` → `"3.14"`.
- Line ~264: `python-version: "3.13"` → `"3.14"`.

- [ ] **Step 8: `.github/workflows/release-nexus-fs.yml`**

`python-version: "3.13"` (line ~62) → `"3.14"`.

- [ ] **Step 9: `.github/workflows/docs-nexus-fs.yml`**

`python-version: "3.13"` (line ~39) → `"3.14"`.

- [ ] **Step 10: `.github/workflows/conda-pack.yml`**

Three changes:

- Leading comment at line ~10: `Create nexus conda env (Python 3.13)` → `Create nexus conda env (Python 3.14)`.
- Step name at line ~65: `Create nexus conda environment (Python 3.13)` → `... Python 3.14)`.
- Command at line ~68: `conda create -n nexus python=3.13 -y` → `python=3.14 -y`.

- [ ] **Step 11: `.github/actions/build-rust-extensions/action.yml`**

Update the stale comment at line ~46:

```yaml
# Before:
        # Ubuntu's apt provides 3.12.x; macOS needs a manual 3.x install.

# After:
        # Python 3.14 is set up by the actions/setup-python step in calling workflows;
        # this action's setup is version-agnostic.
```

- [ ] **Step 12: Sanity grep across workflows**

```bash
grep -rn '3\.1[23]' .github/workflows/ .github/actions/
```

Expected: matches only in issue-template examples (which are updated in Task 14). No `python-version: "3.1[23]"` matches.

- [ ] **Step 13: Commit**

```bash
git add .github/workflows/ .github/actions/
git commit -m "ci: bump all workflows and actions to Python 3.14"
```

---

## Task 13: Update Dockerfiles to `python:3.14-slim`

**Files:**
- `Dockerfile`
- `dockerfiles/Dockerfile.remote`
- `dockerfiles/Dockerfile.minimal`
- `tests/e2e/self_contained/Dockerfile.fuse-test`

- [ ] **Step 1: `Dockerfile`**

Three lines to update:

- Line 21: `FROM python:3.13-slim AS builder` → `FROM python:3.14-slim AS builder`.
- Line 144: `rm -f /usr/local/lib/python3.13/site-packages/hnswlib*.so` → `rm -f /usr/local/lib/python3.14/site-packages/hnswlib*.so`.
- Line 149: `FROM python:3.13-slim` → `FROM python:3.14-slim`.
- Line 179: `ln -sf /usr/local/lib/python3.13/site-packages/torch/lib/libc10.so /usr/lib/libc10.so` → `ln -sf /usr/local/lib/python3.14/site-packages/torch/lib/libc10.so /usr/lib/libc10.so`.
- Line 204: `COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages` → both paths to `python3.14`.

Grep to confirm:

```bash
grep -n 'python3\.1[23]' Dockerfile
```

Expected: no matches.

- [ ] **Step 2: `dockerfiles/Dockerfile.remote`**

Lines 12 and 25: `FROM python:3.13-slim` → `FROM python:3.14-slim`.

- [ ] **Step 3: `dockerfiles/Dockerfile.minimal`**

Lines 12 and 53: `FROM python:3.13-slim` → `FROM python:3.14-slim`.

- [ ] **Step 4: `tests/e2e/self_contained/Dockerfile.fuse-test`**

Line 1: `FROM python:3.12-slim` → `FROM python:3.14-slim`.

- [ ] **Step 5: Confirm no other Dockerfile references 3.12/3.13**

```bash
grep -rn 'python:3\.1[23]' Dockerfile dockerfiles/ tests/
```

Expected: no matches.

- [ ] **Step 6: Build the main image locally (or at least `docker build --no-cache -t nexus:ci-test .`)**

```bash
docker build -t nexus:3.14-test .
```

Expected: clean build on 3.14. If pdf-inspector fails to build, verify `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` env var is set in the Dockerfile (it was added by a prior commit; keep it).

- [ ] **Step 7: Commit**

```bash
git add Dockerfile dockerfiles/ tests/e2e/self_contained/Dockerfile.fuse-test
git commit -m "build(docker): bump all images to python:3.14-slim"
```

---

## Task 14: Update issue templates and discussion template

**Files:**
- `.github/ISSUE_TEMPLATE/question.md`
- `.github/ISSUE_TEMPLATE/performance.md`
- `.github/ISSUE_TEMPLATE/bug_report.md`
- `.github/DISCUSSION_TEMPLATE/q-and-a.yml`

- [ ] **Step 1: Update the three Markdown issue templates**

In each of `question.md:41`, `performance.md:50`, `bug_report.md:39` change:

```
- **Python Version:** [e.g., 3.11.5]
```

to:

```
- **Python Version:** [e.g., 3.14.1]
```

- [ ] **Step 2: `.github/DISCUSSION_TEMPLATE/q-and-a.yml:47`**

```
- Python Version: 3.11.5
```

to:

```
- Python Version: 3.14.1
```

- [ ] **Step 3: Sanity grep**

```bash
grep -rEn '3\.11\.5|e\.g\., 3\.11' .github/
```

Expected: no matches.

- [ ] **Step 4: Commit**

```bash
git add .github/ISSUE_TEMPLATE/ .github/DISCUSSION_TEMPLATE/
git commit -m "docs(templates): bump Python example to 3.14"
```

---

## Task 15: Update README and user-guide

**Files:**
- `README.md`
- `docs/guides/user-guide.md`

- [ ] **Step 1: `README.md`**

Remove any markitdown mentions (install instructions, feature bullets, architecture diagrams).

Add a short Python-version note where install instructions live. Exact wording:

```markdown
### Requirements

- **Python 3.14+** (Nexus dropped support for 3.12/3.13 in vNEXT). Bare-metal
  `pip install nexus` requires a Rust toolchain because `pdf-inspector` builds
  from sdist until upstream ships cp314 wheels. The official Docker image
  ships Rust and handles this automatically.
```

Replace `vNEXT` with the target release tag if known; otherwise leave `vNEXT` and call it out in the PR description as a pre-merge item.

- [ ] **Step 2: `docs/guides/user-guide.md`**

Grep for `markitdown` mentions in this file:

```bash
grep -n -i markitdown docs/guides/user-guide.md
```

For each hit, either:
- Delete the entire sentence/section if it's markitdown-specific.
- Replace "MarkItDown" / "markitdown" with "pdf-inspector" if the surrounding context applies equally to the replacement.

- [ ] **Step 3: Final sanity grep across docs**

```bash
grep -rn -i "markitdown" docs/ README.md
```

Expected: zero matches.

- [ ] **Step 4: Commit**

```bash
git add README.md docs/guides/user-guide.md
git commit -m "docs: drop markitdown references; note Python 3.14 requirement"
```

---

## Task 16: Final validation (Python 3.14, full suite, Docker smoke)

Goal: one consolidated validation pass across the whole change set before opening the PR.

**Files:**
- None (verification only)

- [ ] **Step 1: Rebuild the venv from scratch on 3.14**

```bash
rm -rf .venv-314
python3.14 -m venv .venv-314
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv sync --python .venv-314/bin/python --frozen
```

Expected: clean install.

- [ ] **Step 2: Run the full unit suite**

```bash
.venv-314/bin/python -m pytest
```

Expected: all tests pass.

- [ ] **Step 3: Run mypy**

```bash
.venv-314/bin/python -m mypy src/
```

Expected: same baseline error count as in Task 4 (or lower). Flag any new errors introduced by the 3.14 mypy semantics.

- [ ] **Step 4: Run ruff**

```bash
.venv-314/bin/python -m ruff check .
```

Expected: clean.

- [ ] **Step 5: Final "no markitdown anywhere" sweep**

```bash
grep -rn -i "markitdown" src/ tests/ docs/ README.md .github/ Dockerfile* dockerfiles/ pyproject.toml packages/ rust/
```

Expected: zero matches (excluding git history and the design/plan docs under `docs/superpowers/`).

- [ ] **Step 6: Docker smoke test**

```bash
docker build -t nexus:3.14-smoke .
docker run --rm nexus:3.14-smoke python -c "
import nexus
from nexus.bricks.parsers.brick import ParsersBrick
b = ParsersBrick()
print('parsers ok; registered:', sorted(p.name for p in b._parser_registry._parsers))
"
```

Expected: prints a list containing the pdf-inspector parser's `name` (value returned by `PdfInspectorParser.name` — verify by reading `src/nexus/bricks/parsers/pdf_inspector_parser.py` before running). No exceptions. Exact parser name string will depend on the class's `name` property; the test fails if the list is empty or errors.

- [ ] **Step 7: No commit — this task is a gate only**

---

## Task 17: File tracking issue, draft PR description

Goal: set up the GitHub metadata for merging the change.

**Files:**
- None (GitHub operations only)

- [ ] **Step 1: File the tracking issue**

Title: `Drop Python 3.12/3.13 support; raise floor to 3.14; remove markitdown`.

Body:

```
## Why

- `pdf-inspector` landed in #3789 as the default local PDF parser.
- `markitdown` has no 3.14-compatible sdist (upstream blocker on onnxruntime/magika).
- Supporting 3.12, 3.13, and 3.14 in parallel means three separate parse-stack matrices in `pyproject.toml` core deps, none of which covers 3.14 out of the box.
- Collapsing to a single 3.14 floor deletes the matrix, removes ~200 MB of optional markitdown transitive deps, and commits to the pdf-inspector + Docker + forward-compat env var install path.

## Breaking changes

- `requires-python = ">=3.14"`. Installs on 3.12/3.13 will fail with `pip`'s standard "Package requires a different Python" error.
- `nexus[markitdown]` extra removed.
- Office / HTML / EPUB / audio parsing no longer available by default. Users who need those formats bring their own `ParseProvider`.
- `[compression]` extra removed. Nexus uses Python 3.14's stdlib `compression.zstd`.

## Scope

See [design spec](docs/superpowers/specs/2026-04-16-python-3.14-floor-drop-markitdown-design.md) and [implementation plan](docs/superpowers/plans/2026-04-17-python-3.14-floor-drop-markitdown.md).

Supersedes #3757 (which merged as "optional markitdown" in #3789; with the floor at 3.14 the optional slot becomes deletion).
```

Record the issue number for the PR description.

- [ ] **Step 2: Push the branch and open the PR**

```bash
git push -u origin $(git branch --show-current)
gh pr create --title "build: raise Python floor to 3.14 and remove markitdown" --body "$(cat <<'EOF'
Closes #<issue-number-from-step-1>
Supersedes #3757 (merged as "optional markitdown" in #3789).

## Summary

- `requires-python = ">=3.14"` in every pyproject (`pyproject.toml`, `packages/nexus-fs/`, `rust/{kernel,raft,tasks}/`).
- `pdf-inspector>=0.1.1` unconditional; no more `python_version == '3.12'` marker.
- Deleted: `markitdown_parser.py`, `markitdown_provider.py`, the `[markitdown]` optional extra, every markitdown reference in source / tests / docs.
- Deleted: the `compression.zstd` `zstandard` fallback (both pin and code); 3.14 stdlib handles it.
- Every CI workflow and Dockerfile bumped to Python 3.14.
- `mypy.python_version = "3.14"` in root + `packages/nexus-fs/`.
- `uv.lock` regenerated on Python 3.14.

## Breaking changes

- `pip install nexus` requires Python 3.14. Installs on 3.12/3.13 fail with pip's version-mismatch error.
- `nexus[markitdown]` extra removed. Office/HTML/EPUB/audio parsing no longer available out of the box.
- `nexus[compression]` extra removed. Stdlib `compression.zstd` used instead.
- Bare-metal `pip install nexus` now needs a Rust toolchain because `pdf-inspector` builds from sdist. Docker image ships Rust; normal dev setups have Rust.

## Test plan

- [x] `pytest` green on Python 3.14 (full unit suite)
- [x] `mypy src/` no new errors
- [x] `ruff check .` clean
- [x] Docker smoke test: `docker build` + `ParsersBrick()` instantiation prints pdf-inspector in registered parsers
- [x] `grep -rn -i markitdown <tree>` returns zero matches (excluding design/plan docs)

## Spec + plan

- Design: [docs/superpowers/specs/2026-04-16-python-3.14-floor-drop-markitdown-design.md](docs/superpowers/specs/2026-04-16-python-3.14-floor-drop-markitdown-design.md)
- Plan: [docs/superpowers/plans/2026-04-17-python-3.14-floor-drop-markitdown.md](docs/superpowers/plans/2026-04-17-python-3.14-floor-drop-markitdown.md)
EOF
)"
```

Fill in `<issue-number-from-step-1>` before running.

- [ ] **Step 3: Update the plan file**

After the PR URL is known, edit this plan's header to include `**PR:** <url>`. Commit.

---

## Completion checklist

- [ ] Task 1: baseline verified on 3.14
- [ ] Task 2: root pyproject `requires-python` + classifiers bumped
- [ ] Task 3: sibling pyprojects bumped
- [ ] Task 4: mypy `python_version = "3.14"` everywhere
- [ ] Task 5: conditional parser pins collapsed into unconditional `pdf-inspector`
- [ ] Task 6: `[markitdown]` extra deleted
- [ ] Task 7: markitdown parser + provider files deleted, registrations removed
- [ ] Task 8: docstring / comment / env-loader refs purged
- [ ] Task 9: `compression.zstd` fallback removed (code + pin)
- [ ] Task 10: tests updated (delete / rename / retarget)
- [ ] Task 11: `uv.lock` regenerated on 3.14
- [ ] Task 12: every CI workflow on 3.14
- [ ] Task 13: every Dockerfile on `python:3.14-slim`
- [ ] Task 14: issue + discussion templates updated
- [ ] Task 15: README + user-guide updated
- [ ] Task 16: full validation pass green
- [ ] Task 17: tracking issue filed, PR opened, plan updated with PR URL
