python-docs-mcp-server 0.1.5__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/INTEGRATION-TEST.md +4 -0
  2. python_docs_mcp_server-0.2.0/.github/RELEASE.md +114 -0
  3. python_docs_mcp_server-0.2.0/.github/TEST-STRATEGY.md +101 -0
  4. python_docs_mcp_server-0.2.0/.planning/ROADMAP.md +23 -0
  5. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-01-data-shape-spike-PLAN.md +205 -0
  6. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md +181 -0
  7. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-02-result-models-PLAN.md +183 -0
  8. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-02-result-models-SUMMARY.md +97 -0
  9. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-03-compare-service-PLAN.md +403 -0
  10. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-03-compare-service-SUMMARY.md +158 -0
  11. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-04-mcp-tool-wiring-PLAN.md +405 -0
  12. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-04-mcp-tool-wiring-SUMMARY.md +157 -0
  13. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-05-user-facing-docs-PLAN.md +254 -0
  14. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-05-user-facing-docs-SUMMARY.md +95 -0
  15. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-REVIEW.md +240 -0
  16. python_docs_mcp_server-0.2.0/.planning/phases/09-compare-versions/09-VERIFICATION.md +120 -0
  17. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/CHANGELOG.md +45 -0
  18. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/PKG-INFO +4 -3
  19. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/README.md +3 -2
  20. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/pyproject.toml +1 -1
  21. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/server.json +3 -3
  22. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/app_context.py +2 -0
  23. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/models.py +87 -0
  24. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/server.py +37 -0
  25. python_docs_mcp_server-0.2.0/src/mcp_server_python_docs/services/compare.py +303 -0
  26. python_docs_mcp_server-0.2.0/tests/test_compare_versions.py +380 -0
  27. python_docs_mcp_server-0.2.0/tests/test_compare_versions_spike.py +193 -0
  28. python_docs_mcp_server-0.2.0/tests/test_detection.py +125 -0
  29. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_retrieval_regression.py +4 -1
  30. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_services.py +10 -3
  31. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/uv.lock +31 -31
  32. python_docs_mcp_server-0.1.5/.github/RELEASE.md +0 -198
  33. python_docs_mcp_server-0.1.5/.planning/ROADMAP.md +0 -20
  34. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.cursorignore +0 -0
  35. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/dependabot.yml +0 -0
  36. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/ci.yml +0 -0
  37. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/codeql.yml +0 -0
  38. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/e2e.yml +0 -0
  39. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/release.yml +0 -0
  40. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/scorecard.yml +0 -0
  41. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.github/workflows/security.yml +0 -0
  42. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.gitignore +0 -0
  43. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.planning/POSITIONING.md +0 -0
  44. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.planning/phases/09-compare-versions/09-CONTEXT.md +0 -0
  45. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.planning/phases/10-whatsnew/10-CONTEXT.md +0 -0
  46. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/.planning/phases/11-detect-venv/11-CONTEXT.md +0 -0
  47. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/AGENTS.md +0 -0
  48. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/CLAUDE.md +0 -0
  49. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/CONTRIBUTING.md +0 -0
  50. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/LICENSE +0 -0
  51. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/SECURITY.md +0 -0
  52. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/glama.json +0 -0
  53. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/__init__.py +0 -0
  54. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/__main__.py +0 -0
  55. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/data/synonyms.yaml +0 -0
  56. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/detection.py +0 -0
  57. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/diagnostics.py +0 -0
  58. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/errors.py +0 -0
  59. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/ingestion/__init__.py +0 -0
  60. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/ingestion/cpython_versions.py +0 -0
  61. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/ingestion/inventory.py +0 -0
  62. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/ingestion/publish.py +0 -0
  63. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/ingestion/sphinx_json.py +0 -0
  64. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/py.typed +0 -0
  65. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/retrieval/__init__.py +0 -0
  66. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/retrieval/budget.py +0 -0
  67. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/retrieval/query.py +0 -0
  68. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/retrieval/ranker.py +0 -0
  69. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/__init__.py +0 -0
  70. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/cache.py +0 -0
  71. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/content.py +0 -0
  72. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/observability.py +0 -0
  73. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/package_docs.py +0 -0
  74. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/persistent_cache.py +0 -0
  75. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/search.py +0 -0
  76. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/version.py +0 -0
  77. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/services/version_resolution.py +0 -0
  78. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/storage/__init__.py +0 -0
  79. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/storage/db.py +0 -0
  80. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/src/mcp_server_python_docs/storage/schema.sql +0 -0
  81. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/__init__.py +0 -0
  82. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/conftest.py +0 -0
  83. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/retrieval_regression_cases.json +0 -0
  84. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/sample_broken.fjson +0 -0
  85. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/sample_library.fjson +0 -0
  86. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/schema-get_docs-input.json +0 -0
  87. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/schema-get_docs-output.json +0 -0
  88. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/schema-list_versions-output.json +0 -0
  89. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/schema-search_docs-input.json +0 -0
  90. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/fixtures/schema-search_docs-output.json +0 -0
  91. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_ci_workflows.py +0 -0
  92. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_doctor.py +0 -0
  93. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_ingestion.py +0 -0
  94. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_mcp_get_docs_cache_smoke.py +0 -0
  95. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_multi_version.py +0 -0
  96. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_package_docs.py +0 -0
  97. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_packaging.py +0 -0
  98. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_persistent_docs_cache.py +0 -0
  99. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_phase1_integration.py +0 -0
  100. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_publish.py +0 -0
  101. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_retrieval.py +0 -0
  102. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_schema.py +0 -0
  103. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_schema_snapshot.py +0 -0
  104. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_stability.py +0 -0
  105. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_stdio_hygiene.py +0 -0
  106. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_stdio_smoke.py +0 -0
  107. {python_docs_mcp_server-0.1.5 → python_docs_mcp_server-0.2.0}/tests/test_synonyms.py +0 -0
@@ -38,8 +38,10 @@ npx @modelcontextprotocol/inspector uv --directory . run python-docs-mcp-server
38
38
  - [ ] Confirm the tool list includes:
39
39
  - `search_docs`
40
40
  - `get_docs`
41
+ - `lookup_package_docs`
41
42
  - `list_versions`
42
43
  - `detect_python_version`
44
+ - `compare_versions`
43
45
  - [ ] Call `search_docs` with query `asyncio.TaskGroup`, `kind="symbol"`, `version="3.13"`
44
46
  - Expected: exact symbol hit with `library/asyncio-task.html`
45
47
  - [ ] Call `get_docs` for the returned slug and anchor
@@ -48,6 +50,8 @@ npx @modelcontextprotocol/inspector uv --directory . run python-docs-mcp-server
48
50
  - Expected: indexed versions appear with the configured default version
49
51
  - [ ] Call `detect_python_version`
50
52
  - Expected: returns local interpreter information without breaking the session
53
+ - [ ] Call `compare_versions` with `symbol="asyncio.TaskGroup"`, `v1="3.10"`, `v2="3.11"`
54
+ - Expected: result with `change="added"` and `new_in="3.11"`, JSON serialization under ~1200 bytes
51
55
  - [ ] Observe no protocol corruption or unexplained disconnects in Inspector
52
56
 
53
57
  ## Test 2: Claude Desktop integration
@@ -0,0 +1,114 @@
1
+ # Release Process
2
+
3
+ ## One-Time Setup: PyPI Trusted Publishing
4
+
5
+ Already configured for this project — these steps are kept for reference when forking or recovering the publisher state.
6
+
7
+ 1. Go to https://pypi.org/manage/account/publishing/
8
+ 2. Add a publisher (pending or active):
9
+ - **PyPI project name**: `python-docs-mcp-server`
10
+ - **Owner**: your GitHub username or org (this repo: `ayhammouda`)
11
+ - **Repository**: `python-docs-mcp-server`
12
+ - **Workflow name**: `release.yml`
13
+ - **Environment name**: `pypi`
14
+ 3. In the GitHub repo, go to Settings > Environments
15
+ 4. Create an environment named `pypi`
16
+ 5. (Optional) Add environment protection rules:
17
+ - Required reviewers (recommended)
18
+ - Deployment branches: only `main` tags
19
+
20
+ ## Coverage notes
21
+
22
+ **Runtime coverage:** the release workflow builds and tests against Python 3.13 only. Python 3.12 is covered by the CI workflow (`ci.yml`), which runs a 2×2 matrix (3.12/3.13 × ubuntu/macos) on every push to `main`. Since tags are created from commits that have already passed CI, 3.12 compatibility is verified before the release workflow runs. This is an accepted trade-off to keep the release artifact pipeline simple (single Python version produces the wheel).
23
+
24
+ **Documentation coverage:** the full docs index target is Python documentation versions 3.10 through 3.14.
25
+
26
+ ---
27
+
28
+ ## Release Checklist
29
+
30
+ Replace `X.Y.Z` below with the version you are releasing (e.g. `0.1.7`). Complete each step in order; do not skip ahead. The release workflow at `.github/workflows/release.yml` runs four jobs in sequence on every `v*` tag push: `build` → `publish` (PyPI) → `publish-mcp-registry` → `github-release`.
31
+
32
+ ### Pre-Release Verification
33
+
34
+ - [ ] All CI tests green on `main`: https://github.com/ayhammouda/python-docs-mcp-server/actions/workflows/ci.yml
35
+ - [ ] Local test suite passes:
36
+ ```bash
37
+ uv run pytest --tb=short -q
38
+ uv run ruff check src/ tests/
39
+ uv run pyright src/
40
+ ```
41
+ - [ ] All four version-bearing files agree on `X.Y.Z` (the release workflow's `Verify tag matches package version` step enforces this; matching here saves a CI round-trip):
42
+ ```bash
43
+ grep '^version' pyproject.toml
44
+ python3 -c 'import json; d = json.load(open("server.json")); print("server.json:", d["version"], "/ packages[0]:", d["packages"][0]["version"])'
45
+ grep '^name = "python-docs-mcp-server"' -A1 uv.lock | head -2
46
+ ```
47
+ - [ ] `server.json` validates against the live MCP Registry schema. MCP Registry enforces stricter limits than PyPI (e.g. `description ≤ 100 chars`); validating before the tag prevents a half-published release (PyPI succeeds while MCP Registry rejects, as happened on v0.1.5 → recovered in v0.1.6):
48
+ ```bash
49
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
50
+ ./mcp-publisher validate server.json
51
+ ```
52
+ Must report `✅ server.json is valid`. If validation fails, shorten `server.json` `description` on `main` before tagging (the `pyproject.toml` description is unbounded by this constraint and can stay longer).
53
+ - [ ] Integration tests from `.github/INTEGRATION-TEST.md` are complete and signed off
54
+ - [ ] `Doctor` subcommand passes:
55
+ ```bash
56
+ uv run python-docs-mcp-server doctor
57
+ ```
58
+ - [ ] `CHANGELOG.md` has a dated entry for `[X.Y.Z]`
59
+
60
+ ### Tag and Release
61
+
62
+ - [ ] Create the annotated tag (replace `X.Y.Z` everywhere):
63
+ ```bash
64
+ git tag -a vX.Y.Z -m "Release vX.Y.Z
65
+
66
+ <one-line summary of what shipped>
67
+
68
+ See CHANGELOG.md for the full entry."
69
+ ```
70
+ - [ ] Push the tag to trigger the release workflow:
71
+ ```bash
72
+ git push origin vX.Y.Z
73
+ ```
74
+ - [ ] Watch the workflow run to completion:
75
+ ```bash
76
+ gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId') --exit-status
77
+ ```
78
+ - [ ] All four jobs green: `build` → `publish` → `publish-mcp-registry` → `github-release`
79
+
80
+ ### Post-Release Verification
81
+
82
+ - [ ] PyPI listing exists with attestation badge: `https://pypi.org/project/python-docs-mcp-server/X.Y.Z/`
83
+ - [ ] Fresh install test from a clean shell (cache-busted so the new version actually resolves):
84
+ ```bash
85
+ uv cache clean python-docs-mcp-server 2>/dev/null || true
86
+ uvx --refresh python-docs-mcp-server@X.Y.Z --version
87
+ # Expected: python-docs-mcp-server X.Y.Z
88
+ ```
89
+ - [ ] Full install flow:
90
+ ```bash
91
+ uvx python-docs-mcp-server build-index --versions 3.10,3.11,3.12,3.13,3.14
92
+ uvx python-docs-mcp-server doctor
93
+ ```
94
+ Both must complete successfully.
95
+ - [ ] MCP Registry shows `X.Y.Z` as the `isLatest` entry:
96
+ ```bash
97
+ curl -s 'https://registry.modelcontextprotocol.io/v0.1/servers?search=python-docs-mcp-server' | \
98
+ python3 -c "import json,sys; d=json.load(sys.stdin); print(next((e['server']['version'] for e in d['servers'] if e['_meta']['io.modelcontextprotocol.registry/official']['isLatest']), 'no latest'))"
99
+ ```
100
+ - [ ] GitHub Release exists at `https://github.com/ayhammouda/python-docs-mcp-server/releases/tag/vX.Y.Z` with wheel + sdist attached and auto-generated notes
101
+ - [ ] **Slow E2E workflow** passes (manually triggered after publish):
102
+ - Run GitHub Actions workflow `Slow E2E` (`.github/workflows/e2e.yml`)
103
+ - Python 3.13 and 3.14 jobs both pass
104
+ - Each job installs the built wheel and runs `build-index --versions 3.10,3.11,3.12,3.13,3.14`, `doctor`, and `validate-corpus`
105
+ - [ ] Claude Desktop manual smoke test:
106
+ Configure `mcpServers` with `uvx python-docs-mcp-server` and verify a known query (e.g. "what is asyncio.TaskGroup") returns a correct hit.
107
+
108
+ ### Release Complete
109
+
110
+ - [ ] All four `release.yml` jobs green for `vX.Y.Z`
111
+ - [ ] PyPI page shows `X.Y.Z` with the "Provenance" attestation badge
112
+ - [ ] MCP Registry `isLatest` entry points at `X.Y.Z`
113
+ - [ ] GitHub Release `vX.Y.Z` exists with both dist artifacts attached
114
+ - [ ] `CHANGELOG.md` `[X.Y.Z]` entry is committed on `main`
@@ -0,0 +1,101 @@
1
+ # Test Strategy
2
+
3
+ Canonical map of **what we test, at which layer, and where the gaps are**.
4
+ Pairs with the how-to-run instructions in `CONTRIBUTING.md` and the manual
5
+ client runbook in `.github/INTEGRATION-TEST.md`.
6
+
7
+ Last verified: 2026-05-29 — 284 tests passing, ruff clean, pyright 0 errors.
8
+
9
+ ## 1. The pyramid (current shape)
10
+
11
+ ```
12
+ / stdio E2E \ 9 tests — real MCP process over stdio
13
+ / integration \ ~38 tests — multi-version, publish, cache, phase1
14
+ / unit / service \ ~230 tests — services, retrieval, ingestion, compare
15
+ / contract + regression \ 38 tests — schema snapshots, stability, curated cases
16
+ ```
17
+
18
+ This is the right shape: a wide, fast base and a thin, slow top. Keep new
19
+ tests pushed **down** the pyramid — only add an stdio E2E test when a bug can
20
+ *only* manifest across the process boundary (framing, lifespan DI, stdout
21
+ hygiene).
22
+
23
+ ## 2. Expected features → coverage map
24
+
25
+ The server exposes **6 MCP tools**. Every tool must have at least one
26
+ behavioral test and appear in the schema snapshot.
27
+
28
+ | Tool / feature | Primary tests | Layer | Status |
29
+ |------------------------|----------------------------------------------------------------------|------------------|--------|
30
+ | `search_docs` | `test_services`, `test_retrieval`, `test_synonyms`, `test_stability` | unit + regression| STRONG |
31
+ | `get_docs` | `test_services`, `test_retrieval`, `test_persistent_docs_cache`, `test_mcp_get_docs_cache_smoke` | unit + integration | STRONG |
32
+ | `list_versions` | `test_services`, `test_multi_version` | unit + integration| GOOD |
33
+ | `compare_versions` | `test_compare_versions` (15), `test_services` | unit | GOOD |
34
+ | `lookup_package_docs` | `test_package_docs` (8) | unit only | THIN |
35
+ | `detect_python_version`| `test_detection` (12) | unit | GOOD |
36
+
37
+ Cross-cutting coverage:
38
+
39
+ - **Schema contract**: `test_schema.py`, `test_schema_snapshot.py` — input/output
40
+ JSON schemas for each tool are frozen as fixtures; a wire-shape change fails CI.
41
+ - **Multi-version routing**: `test_multi_version.py` — version param resolution and
42
+ default fallback across indexed doc sets.
43
+ - **Regression**: `test_retrieval_regression.py` (curated query→expected cases) and
44
+ `test_stability.py` (property-based invariants that survive CPython doc revisions).
45
+ - **Process hygiene**: `test_stdio_smoke.py`, `test_stdio_hygiene.py` — confirm a real
46
+ stdio server starts, answers, and keeps stdout free of non-protocol noise.
47
+ - **Packaging / CI**: `test_packaging.py`, `test_ci_workflows.py` — installable
48
+ artifact + workflow file invariants.
49
+
50
+ ## 3. What to test, by component type
51
+
52
+ - **Services** (`services/`): business logic in isolation against a `tmp_path`
53
+ SQLite fixture. Cover the happy path, every error branch (`DocsServerError`
54
+ subclasses), and token-budget trimming.
55
+ - **Retrieval/ranking** (`retrieval/`): query parsing, FTS5 behavior, ranker
56
+ ordering. Use property assertions (`>= 1 result`, substring match) over exact
57
+ content so upstream doc edits don't break the suite.
58
+ - **Ingestion** (`ingestion/`): parse valid + deliberately broken `.fjson`
59
+ fixtures; assert idempotency on re-publish.
60
+ - **Server layer** (`server.py`): thin — it only delegates to services and maps
61
+ `DocsServerError → ToolError`. Cover that mapping via stdio smoke, not unit tests.
62
+ - **Detection** (`detection.py`): pure environment probing — see gap below.
63
+
64
+ ## 4. Coverage targets
65
+
66
+ No line-coverage gate is enforced (no `pytest-cov` in the dev deps). The bar is
67
+ **behavioral**, not numeric:
68
+
69
+ - Every public tool has ≥1 happy-path + ≥1 error-path test.
70
+ - Every `errors.py` exception type is raised by at least one test.
71
+ - Every wire-facing model is pinned by a schema snapshot.
72
+
73
+ Adopt these as the definition of done for new tools. A line-coverage gate is
74
+ optional future work; if added, target the `services/` and `retrieval/`
75
+ packages, not `server.py` (intentionally thin) or `__main__.py`.
76
+
77
+ ## 5. Known gaps
78
+
79
+ 1. **`detection.py` — CLOSED (2026-05-29).** `tests/test_detection.py` now
80
+ covers all three branches of the fallback chain (`.python-version` file →
81
+ `python3` in PATH → `sys.version_info`), `_parse_major_minor` parsing, and
82
+ `match_to_indexed` — 12 tests. The isolation pattern (`monkeypatch.chdir` to
83
+ escape a real `.python-version`, `monkeypatch.setattr` on `subprocess.run`)
84
+ is the reference for testing order-dependent environment probing.
85
+ 2. **`lookup_package_docs` has no stdio smoke (LOW).** Covered at the service
86
+ layer only; the PyPI-allowlist trust boundary is never exercised end-to-end.
87
+ 3. **No negative version-resolution E2E (LOW).** Unknown-version errors are
88
+ unit-tested but not asserted over the stdio boundary.
89
+
90
+ ## 6. Reference cases — `detection.py` (now implemented in `test_detection.py`)
91
+
92
+ | Case | Expectation |
93
+ |----------------------------------------|------------------------------------------|
94
+ | `.python-version` file present in cwd | returns `(version, ".python-version file")` |
95
+ | `.python-version` malformed / empty | falls through to next source, no crash |
96
+ | no file, `python3` on PATH | returns `(version, "python3 in PATH")` |
97
+ | no file, no `python3` | returns runtime `(X.Y, "server runtime")`|
98
+ | `_parse_major_minor("Python 3.13.2")` | `"3.13"` |
99
+ | `_parse_major_minor("no digits here")` | `None` |
100
+ | `match_to_indexed("3.13", ["3.13"])` | `"3.13"` |
101
+ | `match_to_indexed("3.9", ["3.13"])` | `None` |
@@ -0,0 +1,23 @@
1
+ # Roadmap
2
+
3
+ ## Shipped
4
+
5
+ - **v0.1.4** — pre-PyPI; installed via `uvx --from git+…`.
6
+ - **v0.1.5** — first PyPI release. Outcomes: positioning lockdown ("canonical Python stdlib oracle for AI coding agents — always free, always MIT, token-frugal"); PyPI publish via Trusted Publishing; benchmark harness against competing docs MCPs; coordinated launch post (personal blog, dev.to, Show HN). The GitHub repo rename to `python-stdlib-mcp` was dropped on 2026-05-14 — the repo and PyPI package keep the `python-docs-mcp-server` name.
7
+ - **v0.1.6** — MCP Registry hotfix: trimmed `server.json` description to ≤100 chars ([#30](https://github.com/ayhammouda/python-docs-mcp-server/pull/30)) so the package validates against the MCP Registry schema. Follow-up: RELEASE.md generalized to be version-agnostic ([#35](https://github.com/ayhammouda/python-docs-mcp-server/pull/35), [#36](https://github.com/ayhammouda/python-docs-mcp-server/pull/36)) so the next release follows the documented flow without one-time scaffolding.
8
+
9
+ ## Backlog (post-v0.1.6)
10
+
11
+ Triaged 2026-05-26 — all three phases remain in scope. Phase 10 is up next.
12
+
13
+ | Phase | Title | Tool surface | Issue | Status |
14
+ |-------|-------|--------------|-------|--------|
15
+ | 09 | `compare_versions(symbol, v1, v2)` | New MCP tool | [#32](https://github.com/ayhammouda/python-docs-mcp-server/issues/32) | **✓ Complete** — 5/5 plans, verified 2026-05-29 |
16
+ | 10 | `whatsnew_for_version(version)` | New MCP tool | [#33](https://github.com/ayhammouda/python-docs-mcp-server/issues/33) | Backlog |
17
+ | 11 | `detect_python_version` v2 (venv-aware) | Existing tool enhancement | [#34](https://github.com/ayhammouda/python-docs-mcp-server/issues/34) | Backlog |
18
+
19
+ Phase CONTEXTs at [`phases/09-compare-versions/`](phases/09-compare-versions/09-CONTEXT.md), [`phases/10-whatsnew/`](phases/10-whatsnew/10-CONTEXT.md), [`phases/11-detect-venv/`](phases/11-detect-venv/11-CONTEXT.md). Implementation kickoff: `/gsd-plan-phase 0X`.
20
+
21
+ ## Historical
22
+
23
+ Pre-2026-05-14 GSD workflow artifacts (phases 1–8 for v0.1.0) live in maintainers' local worktrees and are intentionally not tracked in this repo — they remain accessible to those who ran the original GSD workflow locally but are not load-bearing for current implementation work. With v0.1.5 the `.planning/` directory became a tracked, forward-facing surface for new phase CONTEXTs and roadmap entries.
@@ -0,0 +1,205 @@
1
+ ---
2
+ phase: 09-compare-versions
3
+ plan: 01
4
+ type: execute
5
+ wave: 0
6
+ depends_on: []
7
+ files_modified:
8
+ - .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md
9
+ autonomous: true
10
+ requirements:
11
+ - CMPR-01
12
+ must_haves:
13
+ truths:
14
+ - "A reproducible in-memory two-version SQLite fixture (built from the same `bootstrap_schema` + `doc_sets`/`documents`/`sections`/`symbols` insert pattern used by `tests/test_multi_version.py::multi_version_db`) is the source of all probe data — NOT the user's `index.db` cache, which is mutable and environment-dependent."
15
+ - "The fixture seeds three known Sphinx-version-directive prose forms verbatim: `\"New in version 3.11.\"` for asyncio.TaskGroup in 3.11, `\"Changed in version 3.10:\"` for asyncio.run in 3.11, and `\"Deprecated since version 3.12.\"` for at least one symbol. This converts A1 / A2 from \"verify against live data\" into \"document the prose forms RESEARCH §Q3/Q4 already specifies\" — the spike's job is to lock the regex strings, not rediscover them."
16
+ - "If the executor wants to additionally probe a live `index.db` (where one happens to exist), that is documented under `## Optional live-index cross-check` in the SUMMARY but is NEVER required for the spike to be considered complete."
17
+ - "The regex patterns to extract `new_in` / `changed_in` / `deprecated_in` from `sections.content_text` are pinned to verified prose forms with explicit fallback decisions (return None) recorded for each extractor."
18
+ artifacts:
19
+ - path: ".planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md"
20
+ provides: "Locked regex pattern strings + fallback policy for CompareService extractors, sourced from a reproducible fixture, not user-cache state."
21
+ contains: "## Locked regex patterns"
22
+ key_links:
23
+ - from: "09-01-data-shape-spike-SUMMARY.md"
24
+ to: "services/compare.py (Plan 03)"
25
+ via: "regex pattern strings recorded under '## Locked regex patterns'"
26
+ pattern: "Locked regex patterns"
27
+ ---
28
+
29
+ <objective>
30
+ Lock the regex patterns that `CompareService.compare` will use to extract `new_in` / `changed_in` / `deprecated_in` / `see_also` from section markdown, using a reproducible test fixture as the data source — NOT the user's mutable `index.db` cache.
31
+
32
+ Purpose: Plan 03 needs four concrete regex strings (or explicit `None`-fallback decisions) before its executor can write `services/compare.py`. The original spike design pointed at `get_index_path()` (user cache) for data — that is mutable, may be missing, and may force a slow rebuild. RESEARCH §Q3(a)/§Q4(a) already documents the exact post-markdownify prose forms; this spike re-verifies them against a reproducible fixture (mirroring `tests/test_multi_version.py::multi_version_db`) so the evidence file is bit-for-bit re-runnable in CI, on a fresh checkout, and offline.
33
+
34
+ Output: `09-01-data-shape-spike-SUMMARY.md` recording (a) the in-memory fixture used, (b) the post-extraction regex matches per probe, (c) the locked regex literals or fallback decisions, and (d) an optional `## Optional live-index cross-check` if the executor chose to additionally spot-check a real `index.db`.
35
+ </objective>
36
+
37
+ <execution_context>
38
+ @$HOME/.claude/get-shit-done/workflows/execute-plan.md
39
+ @$HOME/.claude/get-shit-done/templates/summary.md
40
+ </execution_context>
41
+
42
+ <context>
43
+ @AGENTS.md
44
+ @.planning/ROADMAP.md
45
+ @.planning/phases/09-compare-versions/09-CONTEXT.md
46
+ @.planning/phases/09-compare-versions/09-RESEARCH.md
47
+ @tests/test_multi_version.py
48
+ @tests/conftest.py
49
+
50
+ <interfaces>
51
+ <!-- The fixture pattern to clone (in-memory, reproducible, no user-cache dependency). -->
52
+
53
+ From src/mcp_server_python_docs/storage/db.py:
54
+ ```python
55
+ def get_readwrite_connection(db_path: Path) -> sqlite3.Connection: ...
56
+ def bootstrap_schema(conn: sqlite3.Connection) -> None: ...
57
+ ```
58
+
59
+ Relevant schema (from src/mcp_server_python_docs/storage/schema.sql):
60
+ ```sql
61
+ CREATE TABLE doc_sets (id, source, version, language, label, is_default, base_url, ...);
62
+ CREATE TABLE documents (id, doc_set_id, uri, slug, title, content_text, char_count, ...);
63
+ CREATE TABLE sections (id, document_id, uri, anchor, heading, level, ordinal, content_text, char_count, ...);
64
+ CREATE TABLE symbols (id, doc_set_id, qualified_name, normalized_name, module, symbol_type, document_id, section_id, uri, anchor, ...);
65
+ ```
66
+
67
+ Fixture pattern from tests/test_multi_version.py lines 17-81:
68
+ ```python
69
+ @pytest.fixture
70
+ def multi_version_db(tmp_path):
71
+ db_path = tmp_path / "test.db"
72
+ conn = get_readwrite_connection(db_path)
73
+ bootstrap_schema(conn)
74
+ # INSERT two doc_sets + documents + sections + symbols
75
+ # INSERT INTO sections_fts(sections_fts) VALUES('rebuild')
76
+ yield conn
77
+ conn.close()
78
+ ```
79
+ </interfaces>
80
+ </context>
81
+
82
+ <tasks>
83
+
84
+ <task type="auto">
85
+ <name>Task 1: Build the in-memory spike fixture with known Sphinx-directive prose</name>
86
+ <read_first>
87
+ - tests/test_multi_version.py lines 17-81 (the multi_version_db fixture pattern — already in context)
88
+ - src/mcp_server_python_docs/storage/db.py (bootstrap_schema + get_readwrite_connection signatures)
89
+ - src/mcp_server_python_docs/storage/schema.sql (full schema — doc_sets / documents / sections / symbols columns)
90
+ - .planning/phases/09-compare-versions/09-RESEARCH.md Q3(a) lines 170-184 (canonical post-markdownify prose forms for `.. versionadded::` / `.. changed::` / `.. deprecated::`) and Q4(a) lines 197-210 (`.. seealso::` prose form)
91
+ </read_first>
92
+ <action>
93
+ Write a standalone Python script `tests/test_compare_versions_spike.py` (or a notebook-style inline `uv run python -c "..."` if the executor prefers; the script form is preferred because it is rerunnable in CI). The script builds a temporary SQLite DB matching the multi_version_db shape, with two doc_sets (`3.10` not-default, `3.11` default) and four section rows carrying the EXACT post-markdownify prose forms that RESEARCH §Q3/§Q4 documents:
94
+
95
+ (a) `(doc_set=3.11, slug='library/asyncio-task.html', anchor='asyncio.TaskGroup')` — `content_text = "An asynchronous context manager holding a group of tasks.\n\nNew in version 3.11."`
96
+ (b) `(doc_set=3.11, slug='library/asyncio-runner.html', anchor='asyncio.run')` — `content_text = "Execute the coroutine and return the result.\n\nChanged in version 3.10: Added support for ..."`
97
+ (c) `(doc_set=3.11, slug='library/somemodule.html', anchor='some.deprecated_func')` — `content_text = "Old API.\n\nDeprecated since version 3.12: use new_func() instead."`
98
+ (d) `(doc_set=3.11, slug='library/pathlib.html', anchor='pathlib.Path')` — `content_text = "Concrete path classes.\n\nSee also\n\n[os.path](library/os.path.html) — Operating system path manipulation.\n[fnmatch](library/fnmatch.html) — Pattern matching."`
99
+
100
+ These prose forms are NOT invented — they are the literal forms RESEARCH §Q3(a) lines 170-184 documents as surviving the markdownify call in `sphinx_json.py:247`. The spike's job is to lock the regex strings against this controlled fixture, not to rediscover what RESEARCH already proved.
101
+
102
+ The script must build the fixture in-memory or under `tempfile.TemporaryDirectory()` — it must NOT touch `get_index_path()` and must NOT trigger a `build-index` rebuild. The script must be re-runnable on a fresh clone with `uv run python tests/test_compare_versions_spike.py` exiting 0.
103
+ </action>
104
+ <verify>
105
+ <automated>uv run python -c "import tempfile, pathlib, sqlite3; from mcp_server_python_docs.storage.db import bootstrap_schema, get_readwrite_connection; td=tempfile.mkdtemp(); p=pathlib.Path(td)/'spike.db'; c=get_readwrite_connection(p); bootstrap_schema(c); c.execute(\"INSERT INTO doc_sets (source, version, language, label, is_default, base_url) VALUES ('python-docs', '3.11', 'en', 'Python 3.11', 1, 'x')\"); ds=c.execute('SELECT id FROM doc_sets').fetchone()[0]; c.execute(\"INSERT INTO documents (doc_set_id, uri, slug, title, content_text, char_count) VALUES (?, 'library/asyncio-task.html', 'library/asyncio-task.html', 't', '', 0)\", (ds,)); d=c.execute('SELECT id FROM documents').fetchone()[0]; c.execute(\"INSERT INTO sections (document_id, uri, anchor, heading, level, ordinal, content_text, char_count) VALUES (?, 'library/asyncio-task.html#asyncio.TaskGroup', 'asyncio.TaskGroup', 'TaskGroup', 2, 0, 'New in version 3.11.', 20)\", (d,)); c.commit(); print('spike fixture OK')"</automated>
106
+ </verify>
107
+ <acceptance_criteria>
108
+ - Source: a standalone re-runnable Python entry point exists (either `tests/test_compare_versions_spike.py` or the SUMMARY embeds the script) that builds the fixture under `tempfile.TemporaryDirectory()` — no user-cache dependency.
109
+ - Source: the script does NOT import `get_index_path`, does NOT call `build-index`, and does NOT touch `~/Library/Caches/mcp-python-docs/` or `~/.cache/mcp-python-docs/`. Verify by grep: the script does NOT contain the string `get_index_path` or `build-index` or `Caches/mcp-python-docs` or `.cache/mcp-python-docs`.
110
+ - CLI: the verify command exits 0 and prints `spike fixture OK`, confirming `bootstrap_schema` + INSERTs work with the prose forms RESEARCH documents.
111
+ </acceptance_criteria>
112
+ <done>A reproducible, offline, no-side-effects fixture builder exists; running it produces the four seeded section rows.</done>
113
+ </task>
114
+
115
+ <task type="auto">
116
+ <name>Task 2: Probe each Sphinx-directive prose form with its candidate regex</name>
117
+ <read_first>
118
+ - .planning/phases/09-compare-versions/09-RESEARCH.md Q3(a)-(c) (regex candidates: `_NEW_IN_RE`, `_CHANGED_IN_RE`, `_DEPRECATED_IN_RE`)
119
+ - .planning/phases/09-compare-versions/09-RESEARCH.md Q4(c) (`_SEE_ALSO_LINK_RE`)
120
+ - The fixture from Task 1 (already in context)
121
+ </read_first>
122
+ <action>
123
+ Against the fixture from Task 1, run each candidate regex from RESEARCH and record the match:
124
+
125
+ | Probe | content_text excerpt | Candidate regex | Expected capture |
126
+ |-------|----------------------|-----------------|------------------|
127
+ | A1 (versionadded) | `"New in version 3.11."` | `r"New in version\s+(\d+\.\d+)"` | `"3.11"` |
128
+ | Sibling 1 (changed) | `"Changed in version 3.10: ..."` | `r"Changed in version\s+(\d+\.\d+)"` | `"3.10"` |
129
+ | Sibling 2 (deprecated) | `"Deprecated since version 3.12: ..."` | `r"Deprecated since version\s+(\d+\.\d+)"` | `"3.12"` |
130
+ | A2 (seealso) | `"See also\n\n[os.path](...)\n[fnmatch](...)"` | `r"\[([^\]]+)\]\("` (anchored to "See also" window) | `["os.path", "fnmatch"]` |
131
+
132
+ For each row: if the candidate regex captures the expected value → record `HOLDS` and lock the literal. If the candidate fails → record `FALSIFIED` and lock the fallback (`None` for scalar extractors, `[]` for `_extract_see_also`). Record the outcome per probe in the SUMMARY's `## A1 result` / `## A2 result` / `## Sibling directive results` sections.
133
+
134
+ Note: against this controlled fixture, all four are expected to HOLD because the fixture seeds the exact prose form RESEARCH documents. The probe is therefore a tautological verification — its real value is producing a self-contained, re-runnable evidence file. The executor MUST still run the probes (do not just copy the expected outcomes) so that the SUMMARY contains real `re.search` outputs.
135
+ </action>
136
+ <verify>
137
+ <automated>uv run python -c "import re; assert re.search(r'New in version\s+(\d+\.\d+)', 'New in version 3.11.').group(1) == '3.11'; assert re.search(r'Changed in version\s+(\d+\.\d+)', 'Changed in version 3.10: x').group(1) == '3.10'; assert re.search(r'Deprecated since version\s+(\d+\.\d+)', 'Deprecated since version 3.12: y').group(1) == '3.12'; assert re.findall(r'\[([^\]]+)\]\(', 'See also\n\n[os.path](x)\n[fnmatch](y)') == ['os.path', 'fnmatch']; print('all 4 probes HOLD')"</automated>
138
+ </verify>
139
+ <acceptance_criteria>
140
+ - CLI: the verify command exits 0 and prints `all 4 probes HOLD`.
141
+ - Source: SUMMARY records each probe's outcome with the actual captured value (or `None`/`[]` if FALSIFIED).
142
+ - Source: the regex strings recorded in SUMMARY match the literals tested by the verify command — no drift between probe and lock.
143
+ </acceptance_criteria>
144
+ <done>All four regex extractors have a HOLDS/FALSIFIED outcome backed by a real `re.search` call against the fixture's seeded prose.</done>
145
+ </task>
146
+
147
+ <task type="auto">
148
+ <name>Task 3: Write 09-01 SUMMARY with locked extractor decisions</name>
149
+ <read_first>
150
+ - $HOME/.claude/get-shit-done/templates/summary.md
151
+ - The probe outputs collected in Tasks 1-2 (already in context)
152
+ </read_first>
153
+ <action>
154
+ Create `.planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md`. Sections required, in order:
155
+
156
+ `## Spike data source` — Document that the spike used an in-memory fixture (NOT the user cache). Include the path-pattern (`tempfile.mkdtemp()` or `tests/test_compare_versions_spike.py`) and a one-line invocation command that reproduces the spike on a fresh checkout.
157
+
158
+ `## Seeded prose forms` — Per-row table mapping `(version, slug, anchor)` to the verbatim `content_text` used for each probe, cross-referenced to RESEARCH §Q3/§Q4.
159
+
160
+ `## A1 result` (HOLDS / FALSIFIED + captured value)
161
+ `## A2 result` (HOLDS / FALSIFIED + captured link labels)
162
+ `## Sibling directive results` (Changed-in / Deprecated-since — HOLDS / FALSIFIED + captured values)
163
+
164
+ `## Locked regex patterns` (exact Python raw-string regex literals, ready to paste into `services/compare.py`):
165
+ - `_NEW_IN_RE = r"New in version\s+(\d+\.\d+)"` (or `None` if A1 falsified)
166
+ - `_CHANGED_IN_RE = r"Changed in version\s+(\d+\.\d+)"`
167
+ - `_DEPRECATED_IN_RE = r"Deprecated since version\s+(\d+\.\d+)"`
168
+ - `_SEE_ALSO_LINK_RE = r"\[([^\]]+)\]\("`
169
+
170
+ `## Fallback policy` — Per extractor, what to return when no match: `None` for scalar extractors (`_extract_new_in`, `_extract_changed_in`, `_extract_deprecated_in`); `[]` for `_extract_see_also`.
171
+
172
+ `## Optional live-index cross-check` — OPTIONAL section. If the executor additionally spot-checked a real `index.db` and the prose forms there match, record it here as supplementary evidence. If the executor skipped this (which is fine), record `Not performed — fixture is the authoritative source per the revised spike scope.`
173
+
174
+ `## Implications for Plan 03` — 1-3 sentences directing Plan 03's executor: "All four extractors HOLD against the seeded fixture — implement as documented in RESEARCH §Q3/Q4. If live `index.db` shows different prose forms during Plan 03 implementation, raise a finding rather than mutating the regex unilaterally."
175
+ </action>
176
+ <verify>
177
+ <automated>test -f .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## Spike data source" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## Locked regex patterns" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## A1 result" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## A2 result" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## Fallback policy" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && grep -q "## Implications for Plan 03" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md && ! grep -q "get_index_path" .planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md</automated>
178
+ </verify>
179
+ <acceptance_criteria>
180
+ - Source: file `.planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md` exists with the seven required section headings exactly.
181
+ - Source: file does NOT contain `get_index_path` (verified by the negative grep above) — confirming the spike did not depend on the user cache.
182
+ - Behavior: Plan 03's executor can copy the regex literals from `## Locked regex patterns` verbatim into `services/compare.py` without re-probing anything.
183
+ </acceptance_criteria>
184
+ <done>The SUMMARY is checked into the worktree (uncommitted is fine — Plan 03 picks it up immediately).</done>
185
+ </task>
186
+
187
+ </tasks>
188
+
189
+ <verification>
190
+ - The spike is reproducible on a fresh clone, offline, with no `index.db` dependency.
191
+ - All four extractor decisions (`new_in`, `changed_in`, `deprecated_in`, `see_also`) are recorded as locked-regex-or-fallback in the SUMMARY.
192
+ - The SUMMARY explicitly documents that the user cache was NOT used as the data source.
193
+ - No source code under `src/` is modified by this plan (it is a pure spike).
194
+ </verification>
195
+
196
+ <success_criteria>
197
+ - `09-01-data-shape-spike-SUMMARY.md` exists at the path declared in `files_modified`.
198
+ - The seven required section headings are present (verified by the Task 3 grep gate).
199
+ - The Implications-for-Plan-03 line tells the next executor exactly what to lock in.
200
+ - The spike artifact is bit-reproducible offline — running the spike script on a clean checkout produces identical probe outputs.
201
+ </success_criteria>
202
+
203
+ <output>
204
+ Create `.planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md` when done.
205
+ </output>