fastapi-mcp-neo 0.13.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 (111) hide show
  1. fastapi_mcp_neo-0.13.0/.github/workflows/ci-skip.yml +25 -0
  2. fastapi_mcp_neo-0.13.0/.github/workflows/ci.yml +107 -0
  3. fastapi_mcp_neo-0.13.0/.github/workflows/publish.yml +59 -0
  4. fastapi_mcp_neo-0.13.0/.gitignore +20 -0
  5. fastapi_mcp_neo-0.13.0/AGENTS.md +109 -0
  6. fastapi_mcp_neo-0.13.0/CHANGELOG.md +771 -0
  7. fastapi_mcp_neo-0.13.0/PKG-INFO +749 -0
  8. fastapi_mcp_neo-0.13.0/README.md +731 -0
  9. fastapi_mcp_neo-0.13.0/SECURITY.md +29 -0
  10. fastapi_mcp_neo-0.13.0/comparison/README.md +203 -0
  11. fastapi_mcp_neo-0.13.0/comparison/bench/env_ours/pyproject.toml +14 -0
  12. fastapi_mcp_neo-0.13.0/comparison/bench/env_theirs/pyproject.toml +10 -0
  13. fastapi_mcp_neo-0.13.0/comparison/bench/merge_results.py +108 -0
  14. fastapi_mcp_neo-0.13.0/comparison/bench/results.json +300 -0
  15. fastapi_mcp_neo-0.13.0/comparison/bench/results_ours.json +173 -0
  16. fastapi_mcp_neo-0.13.0/comparison/bench/results_theirs.json +86 -0
  17. fastapi_mcp_neo-0.13.0/comparison/bench/run_ours.py +218 -0
  18. fastapi_mcp_neo-0.13.0/comparison/bench/run_theirs.py +151 -0
  19. fastapi_mcp_neo-0.13.0/comparison/bench/shared_app.py +130 -0
  20. fastapi_mcp_neo-0.13.0/examples/notes_oauth/.env.example +12 -0
  21. fastapi_mcp_neo-0.13.0/examples/notes_oauth/README.md +166 -0
  22. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/__init__.py +0 -0
  23. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/__main__.py +37 -0
  24. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/auth_routes.py +171 -0
  25. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/config.py +27 -0
  26. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/credentials.py +53 -0
  27. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/main.py +63 -0
  28. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/mcp_oauth.py +85 -0
  29. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/models.py +54 -0
  30. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/notes_routes.py +142 -0
  31. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/observability.py +48 -0
  32. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/session.py +50 -0
  33. fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/store.py +23 -0
  34. fastapi_mcp_neo-0.13.0/examples/notes_oauth/pyproject.toml +35 -0
  35. fastapi_mcp_neo-0.13.0/examples/notes_oauth/scripts/smoke.py +105 -0
  36. fastapi_mcp_neo-0.13.0/examples/otel_smoke.md +122 -0
  37. fastapi_mcp_neo-0.13.0/examples/otel_smoke.py +124 -0
  38. fastapi_mcp_neo-0.13.0/examples/shop/__init__.py +0 -0
  39. fastapi_mcp_neo-0.13.0/examples/shop/__main__.py +27 -0
  40. fastapi_mcp_neo-0.13.0/examples/shop/app.py +587 -0
  41. fastapi_mcp_neo-0.13.0/examples/shop/jwt_passthrough.py +168 -0
  42. fastapi_mcp_neo-0.13.0/examples/shop/mcp_walkthrough.py +104 -0
  43. fastapi_mcp_neo-0.13.0/examples/shop/server.py +31 -0
  44. fastapi_mcp_neo-0.13.0/pyproject.toml +64 -0
  45. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/__init__.py +40 -0
  46. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/depth_guard.py +105 -0
  47. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/domains.py +146 -0
  48. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/graphiql.py +82 -0
  49. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/handler.py +337 -0
  50. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/http_api.py +89 -0
  51. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/invoker.py +370 -0
  52. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/__init__.py +0 -0
  53. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/errors.py +38 -0
  54. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/progressive_tools.py +350 -0
  55. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/server.py +355 -0
  56. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/tools.py +185 -0
  57. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/naming.py +35 -0
  58. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/recursive_expand.py +260 -0
  59. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/scalars.py +110 -0
  60. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/scanner.py +831 -0
  61. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/schema_builder.py +353 -0
  62. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/search.py +104 -0
  63. fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/type_builder.py +1041 -0
  64. fastapi_mcp_neo-0.13.0/tests/__init__.py +0 -0
  65. fastapi_mcp_neo-0.13.0/tests/conftest.py +32 -0
  66. fastapi_mcp_neo-0.13.0/tests/e2e/__init__.py +0 -0
  67. fastapi_mcp_neo-0.13.0/tests/e2e/test_graphiql.py +182 -0
  68. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_auth.py +92 -0
  69. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_headers.py +104 -0
  70. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_mount.py +82 -0
  71. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_progressive.py +266 -0
  72. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_server.py +190 -0
  73. fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_simple_tools.py +137 -0
  74. fastapi_mcp_neo-0.13.0/tests/e2e/test_oauth_flow.py +235 -0
  75. fastapi_mcp_neo-0.13.0/tests/e2e/test_otel_propagation.py +124 -0
  76. fastapi_mcp_neo-0.13.0/tests/guards/__init__.py +0 -0
  77. fastapi_mcp_neo-0.13.0/tests/guards/test_example_public_api.py +39 -0
  78. fastapi_mcp_neo-0.13.0/tests/guards/test_version.py +22 -0
  79. fastapi_mcp_neo-0.13.0/tests/integration/__init__.py +0 -0
  80. fastapi_mcp_neo-0.13.0/tests/integration/test_alias_contract.py +202 -0
  81. fastapi_mcp_neo-0.13.0/tests/integration/test_dataclass.py +321 -0
  82. fastapi_mcp_neo-0.13.0/tests/integration/test_depth_guard.py +107 -0
  83. fastapi_mcp_neo-0.13.0/tests/integration/test_document_cache.py +180 -0
  84. fastapi_mcp_neo-0.13.0/tests/integration/test_handler_execution.py +310 -0
  85. fastapi_mcp_neo-0.13.0/tests/integration/test_handler_runtime.py +204 -0
  86. fastapi_mcp_neo-0.13.0/tests/integration/test_headers_passthrough.py +165 -0
  87. fastapi_mcp_neo-0.13.0/tests/integration/test_json_passthrough.py +104 -0
  88. fastapi_mcp_neo-0.13.0/tests/integration/test_mutation_semantics.py +70 -0
  89. fastapi_mcp_neo-0.13.0/tests/integration/test_param_matrix.py +362 -0
  90. fastapi_mcp_neo-0.13.0/tests/integration/test_recursive_expand.py +739 -0
  91. fastapi_mcp_neo-0.13.0/tests/integration/test_router_edges.py +344 -0
  92. fastapi_mcp_neo-0.13.0/tests/integration/test_scanner_contract.py +57 -0
  93. fastapi_mcp_neo-0.13.0/tests/integration/test_schema_builder.py +26 -0
  94. fastapi_mcp_neo-0.13.0/tests/integration/test_schema_sdl.py +284 -0
  95. fastapi_mcp_neo-0.13.0/tests/integration/test_type_mapping_regressions.py +616 -0
  96. fastapi_mcp_neo-0.13.0/tests/integration/test_typevar_routes.py +145 -0
  97. fastapi_mcp_neo-0.13.0/tests/support/__init__.py +0 -0
  98. fastapi_mcp_neo-0.13.0/tests/support/apps.py +224 -0
  99. fastapi_mcp_neo-0.13.0/tests/support/mcp.py +49 -0
  100. fastapi_mcp_neo-0.13.0/tests/support/models.py +68 -0
  101. fastapi_mcp_neo-0.13.0/tests/unit/__init__.py +0 -0
  102. fastapi_mcp_neo-0.13.0/tests/unit/test_depth_guard.py +68 -0
  103. fastapi_mcp_neo-0.13.0/tests/unit/test_domains.py +113 -0
  104. fastapi_mcp_neo-0.13.0/tests/unit/test_invoker.py +306 -0
  105. fastapi_mcp_neo-0.13.0/tests/unit/test_naming.py +38 -0
  106. fastapi_mcp_neo-0.13.0/tests/unit/test_recursive_expand_walk.py +52 -0
  107. fastapi_mcp_neo-0.13.0/tests/unit/test_scanner.py +604 -0
  108. fastapi_mcp_neo-0.13.0/tests/unit/test_search.py +103 -0
  109. fastapi_mcp_neo-0.13.0/tests/unit/test_type_builder.py +357 -0
  110. fastapi_mcp_neo-0.13.0/uv.lock +2682 -0
  111. fastapi_mcp_neo-0.13.0/why.md +81 -0
@@ -0,0 +1,25 @@
1
+ # Companion to ci.yml. The real CI is path-filtered, so a PR that changes
2
+ # only non-code files (README, CHANGELOG, Comparison, etc.) skips ci.yml
3
+ # entirely — leaving the required "CI All Green" check stuck in "Expected"
4
+ # forever and blocking the PR. This workflow runs on the INVERSE path filter
5
+ # and reports the same "CI All Green" check as passing, so such PRs can
6
+ # merge. On a mixed PR both workflows run; both report green, and a real
7
+ # failure still fails the same-named check, so nothing is masked.
8
+ name: Test
9
+
10
+ on:
11
+ pull_request:
12
+ paths-ignore:
13
+ - 'src/fastapi_mcp_neo/**'
14
+ - 'tests/**'
15
+ - 'examples/**'
16
+ - 'pyproject.toml'
17
+ - 'uv.lock'
18
+
19
+ jobs:
20
+ ci-all-green:
21
+ name: CI All Green
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: No code paths changed
25
+ run: echo "No code paths changed — real CI skipped; reporting green."
@@ -0,0 +1,107 @@
1
+ name: Test
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - 'src/fastapi_mcp_neo/**'
7
+ - 'tests/**'
8
+ - 'examples/**'
9
+ - 'pyproject.toml'
10
+ - 'uv.lock'
11
+ push:
12
+ branches: [master]
13
+ paths:
14
+ - 'src/fastapi_mcp_neo/**'
15
+ - 'tests/**'
16
+ - 'examples/**'
17
+ - 'pyproject.toml'
18
+ - 'uv.lock'
19
+
20
+ jobs:
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ # One version failing must not cancel the others: a canceled job shows
25
+ # as failed even when its tests passed (observed: 3.13 finished green,
26
+ # then got canceled by 3.10's failure with fail-fast's default true).
27
+ fail-fast: false
28
+ matrix:
29
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
30
+
31
+ steps:
32
+ - uses: actions/checkout@v5
33
+
34
+ - name: Set up Python ${{ matrix.python-version }}
35
+ uses: actions/setup-python@v6
36
+ with:
37
+ python-version: ${{ matrix.python-version }}
38
+
39
+ - name: Set up uv
40
+ uses: astral-sh/setup-uv@v7
41
+ with:
42
+ enable-cache: true
43
+
44
+ - name: Install dependencies
45
+ run: uv sync --all-extras
46
+
47
+ - name: Run linter
48
+ run: uv run ruff check src tests
49
+
50
+ - name: Run type checker
51
+ run: uv run mypy src
52
+
53
+ - name: Test with pytest (coverage gate 95%)
54
+ run: |
55
+ uv run pytest tests/ -v --cov=fastapi_mcp_neo \
56
+ --cov-report=term-missing --cov-fail-under=95
57
+
58
+ # Guards the demo (and examples' syntax) against framework API drift. The
59
+ # demo is the framework's first real consumer; if a public API is renamed or
60
+ # removed, this job fails on the PR instead of shipping a demo that only
61
+ # breaks at run time. Critical: uv sync installs the WORKING-COPY framework
62
+ # (editable), so the demo is verified against the code in this PR — not the
63
+ # PyPI release. examples/notes_oauth is a standalone project with its own
64
+ # dependency set, so it is guarded here by compileall (syntax) and by
65
+ # tests/test_example_public_api.py in the test job (public-API-only imports).
66
+ demo-guard:
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v5
70
+
71
+ - name: Set up Python
72
+ uses: actions/setup-python@v6
73
+ with:
74
+ python-version: "3.13"
75
+
76
+ - name: Set up uv
77
+ uses: astral-sh/setup-uv@v7
78
+ with:
79
+ enable-cache: true
80
+
81
+ - name: Install working-copy framework + extras
82
+ run: uv sync --all-extras
83
+
84
+ - name: Demo app builds against working-copy framework
85
+ run: |
86
+ uv run python -c "import examples.shop.app; print(f'demo builds: {len(examples.shop.app.app.routes)} routes')"
87
+
88
+ - name: Compile all example sources
89
+ run: uv run python -m compileall -q examples
90
+
91
+ # Single required-status gate. Branch protection should require this ONE
92
+ # check ("CI All Green") instead of each job (the matrix makes per-job names
93
+ # awkward). It aggregates the real jobs and fails if any did not succeed.
94
+ # A mirror job of the same name lives in ci-skip.yml: when a PR touches no
95
+ # code paths this workflow is skipped entirely, so the mirror reports the
96
+ # same check as green and docs-only PRs are never blocked on a check that
97
+ # path filtering skipped.
98
+ ci-all-green:
99
+ name: CI All Green
100
+ if: always()
101
+ needs: [test, demo-guard]
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - name: Verify required jobs succeeded
105
+ run: |
106
+ echo "test=${{ needs.test.result }} demo-guard=${{ needs.demo-guard.result }}"
107
+ [ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.demo-guard.result }}" = "success" ]
@@ -0,0 +1,59 @@
1
+ name: Publish to PyPI via uv
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+
13
+ permissions:
14
+ contents: write # gh release create needs it; PyPI steps need only read
15
+
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@v5
19
+
20
+ - name: Set up uv
21
+ uses: astral-sh/setup-uv@v7
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v6
25
+ with:
26
+ python-version: "3.13"
27
+
28
+ - name: Build the package
29
+ run: uv build
30
+
31
+ - name: Publish to PyPI
32
+ run: uv publish --token ${{ secrets.PYPI_PUBLISHER }}
33
+
34
+ - name: Create the GitHub release from the CHANGELOG section
35
+ # Same tag, notes extracted from CHANGELOG.md — the release entry
36
+ # must never be forgotten again (0.9.0 shipped to PyPI with no
37
+ # Releases-page entry and looked like a failed release).
38
+ # Idempotent: a re-run skips creation when the entry already exists.
39
+ env:
40
+ GH_TOKEN: ${{ github.token }}
41
+ run: |
42
+ if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
43
+ echo "release $GITHUB_REF_NAME already exists — skipping"
44
+ exit 0
45
+ fi
46
+ VERSION="${GITHUB_REF_NAME#v}"
47
+ awk -v header="## $VERSION " '
48
+ index($0, header) == 1 { found = 1; next }
49
+ found && /^## / { exit }
50
+ found { print }
51
+ ' CHANGELOG.md > release-notes.md
52
+ if [ ! -s release-notes.md ]; then
53
+ echo "no CHANGELOG section for $VERSION — falling back to tag annotation"
54
+ git tag -l --format='%(contents)' "$GITHUB_REF_NAME" > release-notes.md
55
+ fi
56
+ gh release create "$GITHUB_REF_NAME" \
57
+ --title "$GITHUB_REF_NAME" \
58
+ --notes-file release-notes.md \
59
+ --latest
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ dist/
8
+ .DS_Store
9
+ .claude/
10
+
11
+ # consumer example: local env + resolved lock stay untracked
12
+ examples/*/.env
13
+ examples/*/uv.lock
14
+ examples/*/.venv/
15
+ Comparison/bench/*/uv.lock
16
+ .coverage
17
+
18
+ # Claude Code project MCP servers (e.g. notes-demo on localhost:8020) —
19
+ # environment-local dev convenience, not project config
20
+ .mcp.json
@@ -0,0 +1,109 @@
1
+ # Agent.md — release process and attention points
2
+
3
+ Operating instructions for agents (and humans) releasing this repo.
4
+
5
+ ## Release flow
6
+
7
+ Prerequisites: on `master`, in sync with origin, working tree clean, CI green
8
+ (`gh run list`). The Unreleased section of `CHANGELOG.md` should carry the
9
+ changes you are shipping.
10
+
11
+ 1. **Decide the version.** This repo is 0.x: breaking changes bump the minor
12
+ (0.1 → 0.2 → …), additive changes may too, fixes alone patch. Look at the
13
+ Unreleased entries: anything marked *(breaking)* forces a minor bump.
14
+ 2. **Bump the version in TWO places** — this is the classic miss:
15
+
16
+ | File | What to change |
17
+ |---|---|
18
+ | `pyproject.toml` | `version = "X.Y.Z"` (+ `uv lock` to refresh the self-entry) |
19
+ | `CHANGELOG.md` | `## Unreleased` → `## X.Y.Z (YYYY-MM-DD)` |
20
+
21
+ `__version__` needs NO hand-edit: since 0.6.0 it is **derived from
22
+ installed package metadata** (`importlib.metadata`), so pyproject is the
23
+ single source of truth. (0.5.0 shipped with a hardcoded `__version__`
24
+ still at 0.4.0 — the published package reported the wrong runtime
25
+ version; deriving removed that release step entirely.)
26
+ `tests/guards/test_version.py` fails the suite if `__version__` and pyproject
27
+ ever disagree again.
28
+
29
+ Verify before committing:
30
+
31
+ ```bash
32
+ grep '^version' pyproject.toml
33
+ uv run python -c "import fastapi_mcp_neo; print(fastapi_mcp_neo.__version__)"
34
+ # both must print the same X.Y.Z
35
+ ```
36
+
37
+ 3. **Commit**: `chore(release): X.Y.Z`, one-line summary of the wave.
38
+ 4. **Tag and push**:
39
+
40
+ ```bash
41
+ git tag vX.Y.Z
42
+ git push origin master vX.Y.Z
43
+ ```
44
+
45
+ 5. **Publish is automatic.** The `v*` tag triggers `.github/workflows/publish.yml`:
46
+ `uv build` → `uv publish` (token from the `PYPI_PUBLISHER` secret). No
47
+ GitHub Release is created — the CHANGELOG is the release notes. Watch it:
48
+
49
+ ```bash
50
+ gh run watch $(gh run list --workflow publish.yml --limit 1 --json databaseId -q '.[0].databaseId')
51
+ ```
52
+
53
+ 6. **Verify before announcing**:
54
+
55
+ ```bash
56
+ curl -s https://pypi.org/pypi/fastapi-mcp-neo/json | python3 -c \
57
+ "import sys,json; print(json.load(sys.stdin)['info']['version'])"
58
+ pip download fastapi-mcp-neo==X.Y.Z --no-deps -d /tmp/check && rm -rf /tmp/check
59
+ ```
60
+
61
+ Stronger check (used for 0.4.0): run the example off the published wheel —
62
+ from `examples/notes_oauth`, bypassing the editable source:
63
+
64
+ ```bash
65
+ OTEL_OTLP_ENDPOINT=http://localhost:4317 uv run --no-project \
66
+ --with 'fastapi-mcp-neo[mcp]==X.Y.Z' \
67
+ --with fastapi --with uvicorn --with python-dotenv --with asgi-lifespan \
68
+ --with opentelemetry-sdk --with opentelemetry-exporter-otlp \
69
+ python -c "import fastapi_mcp_neo; assert 'site-packages' in fastapi_mcp_neo.__file__; from app.__main__ import main; main()"
70
+ ```
71
+
72
+ 7. **After release**: new work goes under a fresh `## Unreleased` heading in
73
+ the CHANGELOG (Added / Fixed / Changed subsections).
74
+
75
+ ## Attention points
76
+
77
+ - **Release only from `master`.** Feature branches merge (fast-forward when
78
+ linear) before any version bump.
79
+ - **Test suite layers.** `tests/unit/` pins one module without handler
80
+ execution, `tests/integration/` goes through `RouterGraphQLHandler`,
81
+ `tests/e2e/` drives ASGI / fastmcp clients, `tests/guards/` holds meta
82
+ pins. Shared sample models, app factories, and protocol helpers live in
83
+ `tests/support/` — import via `from tests.support import apps, models`
84
+ (every directory keeps an `__init__.py`). New tests go where their
85
+ SUBJECT lives, not where the ticket came from; wave/ticket IDs belong in
86
+ docstrings, never in test names.
87
+ - **The tag is the trigger.** Pushing the tag publishes to PyPI — a broken
88
+ release cannot be unpublished, only superseded by the next version. Never
89
+ move or delete a published tag.
90
+ - **PyPI token**: `PYPI_PUBLISHER` in repo secrets. First release of a new
91
+ project name needs an account-wide token; afterwards rotate to a
92
+ project-scoped one. Also claim the project on PyPI after first publish.
93
+ - **`__version__` consistency** is enforced by `tests/guards/test_version.py`
94
+ (derived from package metadata; pyproject is the single source) — the
95
+ wheel's runtime string must match the tag.
96
+ - **CHANGELOG discipline**: entries land in Unreleased as they ship, not in
97
+ a batch at release time; each entry says *what changed and why*, breaking
98
+ changes are marked *(breaking)*.
99
+ - **Commit hygiene**: conventional commits (`feat:`, `fix:`, `docs:`,
100
+ `chore(release):`); CHANGELOG hunks split into the commit they describe
101
+ when a release groups several commits.
102
+ - **Coverage gate**: CI runs pytest with `--cov-fail-under=95` (baseline
103
+ 96.7% after the layered-suite refactor); local `uv run pytest` stays
104
+ ungated for fast partial runs.
105
+ - **Locks**: the repo-root `uv.lock` is committed; `examples/*` and
106
+ `comparison/bench/*` locks are ignored (they carry local path sources).
107
+ - **Known trap**: port 8020 — check `lsof -nP -iTCP:8020 -sTCP:LISTEN`
108
+ before "the server won't start" debugging; yesterday's instance may still
109
+ be holding it (`python -c`-launched processes dodge `pkill -f "python -m app"`).