modern-python-guidance 0.3.5__tar.gz → 0.3.6__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 (132) hide show
  1. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/CHANGELOG.md +16 -0
  2. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/PKG-INFO +11 -11
  3. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/README.md +10 -10
  4. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/pyproject.toml +3 -2
  5. modern_python_guidance-0.3.6/rules/modern-python.md +70 -0
  6. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/__init__.py +1 -1
  7. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/cli.py +12 -6
  8. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/setup_cmd.py +106 -2
  9. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/uninstall_cmd.py +49 -2
  10. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_guide_structure.py +51 -0
  11. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_setup.py +192 -37
  12. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_uninstall.py +117 -35
  13. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/check-python-release.yml +0 -0
  14. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/ci.yml +0 -0
  15. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.github/workflows/publish.yml +0 -0
  16. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/.gitignore +0 -0
  17. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/CONTRIBUTING.md +0 -0
  18. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/LICENSE +0 -0
  19. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/LICENSE-MIT +0 -0
  20. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/SECURITY.md +0 -0
  21. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/edge-cases/opus48_multiline.py +0 -0
  22. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/edge-cases/valid_alt_patterns.py +0 -0
  23. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
  24. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/app.py +0 -0
  25. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/config.py +0 -0
  26. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
  27. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/models.py +0 -0
  28. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
  29. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
  30. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
  31. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/setup.py +0 -0
  32. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
  33. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
  34. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
  35. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
  36. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
  37. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
  38. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
  39. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
  40. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
  41. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
  42. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
  43. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
  44. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/mcp-config.json +0 -0
  45. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v2.txt +0 -0
  46. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v3-mcp.txt +0 -0
  47. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v3.txt +0 -0
  48. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-a.txt +0 -0
  49. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-b.txt +0 -0
  50. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt-v4-c.txt +0 -0
  51. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompt.txt +0 -0
  52. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-detailed.txt +0 -0
  53. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-normal.txt +0 -0
  54. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-a-terse.txt +0 -0
  55. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-detailed.txt +0 -0
  56. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-normal.txt +0 -0
  57. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-b-terse.txt +0 -0
  58. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-detailed.txt +0 -0
  59. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-normal.txt +0 -0
  60. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/prompts/v5-c-terse.txt +0 -0
  61. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-mcp.sh +0 -0
  62. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-v4.sh +0 -0
  63. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run-v5.sh +0 -0
  64. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/run.sh +0 -0
  65. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v2.sh +0 -0
  66. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v3.sh +0 -0
  67. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score-v4.sh +0 -0
  68. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score.sh +0 -0
  69. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/score_v5.py +0 -0
  70. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/bench/test-scorer.sh +0 -0
  71. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-evaluation.md +0 -0
  72. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-procedure.md +0 -0
  73. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/benchmark-v5.md +0 -0
  74. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/docs/design.md +0 -0
  75. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/SKILL.md +0 -0
  76. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
  77. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
  78. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
  79. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
  80. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
  81. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
  82. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
  83. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
  84. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
  85. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
  86. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
  87. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
  88. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
  89. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
  90. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
  91. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
  92. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
  93. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
  94. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
  95. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
  96. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
  97. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
  98. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
  99. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
  100. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
  101. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
  102. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
  103. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/template-strings.md +0 -0
  104. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
  105. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
  106. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
  107. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
  108. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
  109. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
  110. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/deferred-annotations.md +0 -0
  111. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
  112. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
  113. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
  114. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
  115. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
  116. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
  117. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/__main__.py +0 -0
  118. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/compat.py +0 -0
  119. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/frontmatter.py +0 -0
  120. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/guide_index.py +0 -0
  121. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/mcp_server.py +0 -0
  122. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/retrieve.py +0 -0
  123. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/search.py +0 -0
  124. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/src/modern_python_guidance/version_detect.py +0 -0
  125. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_cli_integration.py +0 -0
  126. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_frontmatter.py +0 -0
  127. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_mcp_server.py +0 -0
  128. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_retrieve.py +0 -0
  129. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_scorer_v5.py +0 -0
  130. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_search.py +0 -0
  131. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_skill_sync.py +0 -0
  132. {modern_python_guidance-0.3.5 → modern_python_guidance-0.3.6}/tests/test_version_detect.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.6] — 2026-05-31
6
+
7
+ ### Added
8
+
9
+ - Rule-based delivery via symlink: `mpg setup` creates `.claude/rules/modern-python.md` that auto-injects modern Python guidance whenever Python-related files are touched, replacing reliance on probabilistic skill matching (closes #79)
10
+ - `setup_rules()` / `uninstall_rules()` mirroring skills symlink pattern
11
+ - `source.is_symlink()` security check to refuse symlink-to-symlink chains
12
+ - CI sync test enforcing SKILL.md body == rule body consistency
13
+ - 21 new tests (V-037 to V-060) for setup, uninstall, CI sync, and security
14
+
15
+ ### Changed
16
+
17
+ - `--skills-only` now includes Rules (both are project-local artifacts)
18
+ - README updated to document 4 delivery methods (was 3)
19
+ - `--project-dir` help text updated to mention Skills/Rules symlinks
20
+
5
21
  ## [0.3.5] — 2026-05-30
6
22
 
7
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modern-python-guidance
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python
5
5
  Project-URL: Homepage, https://github.com/yottayoshida/modern-python-guidance
6
6
  Project-URL: Repository, https://github.com/yottayoshida/modern-python-guidance
@@ -44,7 +44,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 41 versio
44
44
  - **Measurable impact**: AI writes modern Python 98% of the time with mpg, vs 79% without — even with vague prompts (Opus 4.8, [V5 benchmark details](docs/benchmark-v5.md))
45
45
  - **41 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
46
46
  - **Version-aware**: auto-detects your project's Python version and filters guides accordingly
47
- - **3 delivery methods**: MCP server, CLI, Agent Skills plugin
47
+ - **4 delivery methods**: MCP server, CLI, Agent Skills, and Rules (auto-injects on `.py` file touch)
48
48
  - **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
49
49
 
50
50
  > **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
@@ -58,7 +58,7 @@ pip install modern-python-guidance
58
58
  mpg setup
59
59
  ```
60
60
 
61
- This registers the MCP server and links Agent Skills in one command. Start a new Claude Code session afterwards — newly registered MCP servers and skills take effect on the next launch.
61
+ This registers the MCP server, links Agent Skills, and creates a Rules file (`.claude/rules/modern-python.md`) in one command. The Rules file auto-injects modern Python guidance whenever you touch Python-related files. Start a new Claude Code session afterwards — newly registered MCP servers, skills, and rules take effect on the next launch.
62
62
 
63
63
  ### CLI
64
64
 
@@ -90,7 +90,7 @@ claude mcp add mpg -- mpg mcp
90
90
  }
91
91
  ```
92
92
 
93
- **Agent Skills symlink (Claude Code):**
93
+ **Agent Skills + Rules only (Claude Code):**
94
94
  ```bash
95
95
  mpg setup --skills-only
96
96
  ```
@@ -99,25 +99,25 @@ mpg setup --skills-only
99
99
  | Flag | Purpose |
100
100
  |------|---------|
101
101
  | `--mcp-only` | MCP registration only |
102
- | `--skills-only` | Agent Skills symlink only |
102
+ | `--skills-only` | Project-local artifacts only (Skills + Rules) |
103
103
  | `--scope {user,local}` | MCP scope (default: user) |
104
- | `--project-dir PATH` | Target project for Skills symlink |
104
+ | `--project-dir PATH` | Target project for Skills/Rules symlinks |
105
105
  | `--dry-run` | Show what would be done |
106
106
 
107
- **Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
107
+ **Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills + Rules):
108
108
  ```bash
109
- mpg uninstall # remove both
109
+ mpg uninstall # remove all
110
110
  mpg uninstall --dry-run # preview what would be removed
111
111
  ```
112
112
 
113
113
  | Flag | Purpose |
114
114
  |------|---------|
115
115
  | `--mcp-only` | MCP deregistration only |
116
- | `--skills-only` | Agent Skills unlink only |
117
- | `--project-dir PATH` | Target project for the Skills symlink |
116
+ | `--skills-only` | Project-local artifacts only (Skills + Rules) |
117
+ | `--project-dir PATH` | Target project for Skills/Rules symlinks |
118
118
  | `--dry-run` | Show what would be done |
119
119
 
120
- `mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlink mpg created (never its target or other skills), and is idempotent — running it on an already-clean state is a harmless no-op.
120
+ `mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlinks mpg created (never their targets or other files), and is idempotent — running it on an already-clean state is a harmless no-op.
121
121
 
122
122
  </details>
123
123
 
@@ -12,7 +12,7 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 41 versio
12
12
  - **Measurable impact**: AI writes modern Python 98% of the time with mpg, vs 79% without — even with vague prompts (Opus 4.8, [V5 benchmark details](docs/benchmark-v5.md))
13
13
  - **41 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
14
14
  - **Version-aware**: auto-detects your project's Python version and filters guides accordingly
15
- - **3 delivery methods**: MCP server, CLI, Agent Skills plugin
15
+ - **4 delivery methods**: MCP server, CLI, Agent Skills, and Rules (auto-injects on `.py` file touch)
16
16
  - **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
17
17
 
18
18
  > **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
@@ -26,7 +26,7 @@ pip install modern-python-guidance
26
26
  mpg setup
27
27
  ```
28
28
 
29
- This registers the MCP server and links Agent Skills in one command. Start a new Claude Code session afterwards — newly registered MCP servers and skills take effect on the next launch.
29
+ This registers the MCP server, links Agent Skills, and creates a Rules file (`.claude/rules/modern-python.md`) in one command. The Rules file auto-injects modern Python guidance whenever you touch Python-related files. Start a new Claude Code session afterwards — newly registered MCP servers, skills, and rules take effect on the next launch.
30
30
 
31
31
  ### CLI
32
32
 
@@ -58,7 +58,7 @@ claude mcp add mpg -- mpg mcp
58
58
  }
59
59
  ```
60
60
 
61
- **Agent Skills symlink (Claude Code):**
61
+ **Agent Skills + Rules only (Claude Code):**
62
62
  ```bash
63
63
  mpg setup --skills-only
64
64
  ```
@@ -67,25 +67,25 @@ mpg setup --skills-only
67
67
  | Flag | Purpose |
68
68
  |------|---------|
69
69
  | `--mcp-only` | MCP registration only |
70
- | `--skills-only` | Agent Skills symlink only |
70
+ | `--skills-only` | Project-local artifacts only (Skills + Rules) |
71
71
  | `--scope {user,local}` | MCP scope (default: user) |
72
- | `--project-dir PATH` | Target project for Skills symlink |
72
+ | `--project-dir PATH` | Target project for Skills/Rules symlinks |
73
73
  | `--dry-run` | Show what would be done |
74
74
 
75
- **Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
75
+ **Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills + Rules):
76
76
  ```bash
77
- mpg uninstall # remove both
77
+ mpg uninstall # remove all
78
78
  mpg uninstall --dry-run # preview what would be removed
79
79
  ```
80
80
 
81
81
  | Flag | Purpose |
82
82
  |------|---------|
83
83
  | `--mcp-only` | MCP deregistration only |
84
- | `--skills-only` | Agent Skills unlink only |
85
- | `--project-dir PATH` | Target project for the Skills symlink |
84
+ | `--skills-only` | Project-local artifacts only (Skills + Rules) |
85
+ | `--project-dir PATH` | Target project for Skills/Rules symlinks |
86
86
  | `--dry-run` | Show what would be done |
87
87
 
88
- `mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlink mpg created (never its target or other skills), and is idempotent — running it on an already-clean state is a harmless no-op.
88
+ `mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlinks mpg created (never their targets or other files), and is idempotent — running it on an already-clean state is a harmless no-op.
89
89
 
90
90
  </details>
91
91
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modern-python-guidance"
7
- version = "0.3.5"
7
+ version = "0.3.6"
8
8
  description = "Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0 OR MIT"
@@ -49,11 +49,12 @@ Issues = "https://github.com/yottayoshida/modern-python-guidance/issues"
49
49
 
50
50
  [tool.hatch.build.targets.wheel]
51
51
  packages = ["src/modern_python_guidance"]
52
- only-include = ["src/modern_python_guidance", "skills"]
52
+ only-include = ["src/modern_python_guidance", "skills", "rules"]
53
53
 
54
54
  [tool.hatch.build.targets.wheel.sources]
55
55
  "src" = ""
56
56
  "skills" = "modern_python_guidance/skills"
57
+ "rules" = "modern_python_guidance/rules"
57
58
 
58
59
  [tool.ruff]
59
60
  target-version = "py311"
@@ -0,0 +1,70 @@
1
+ ---
2
+ paths: ["**/*.py", "*.py", "**/pyproject.toml", "**/requirements*.txt", "**/setup.py", "**/setup.cfg", "**/.python-version", "**/Pipfile"]
3
+ ---
4
+
5
+ # Modern Python Guidance
6
+
7
+ Pre-generation design patterns that Ruff cannot auto-fix. Apply these transformations when writing or reviewing Python code.
8
+
9
+ ## When to use
10
+
11
+ - Writing new Python code (use modern patterns from the start)
12
+ - Reviewing Python code (flag outdated patterns)
13
+ - Migrating from Pydantic V1 to V2
14
+ - Upgrading Python version (check which new features are available)
15
+ - Replacing legacy tooling (setup.py, flake8, pip)
16
+
17
+ ## Embedded patterns (high-frequency, Ruff-uncovered)
18
+
19
+ ### Pydantic V2 (>=3.9)
20
+
21
+ - `@validator("f")` → `@field_validator("f")`
22
+ - `@root_validator` → `@model_validator(mode="after")`
23
+ - `class Config:` → `model_config = ConfigDict(...)`
24
+ - `orm_mode` → `from_attributes`, `allow_population_by_field_name` → `populate_by_name`
25
+ - `.parse_obj(d)` → `.model_validate(d)`, `.parse_raw(j)` → `.model_validate_json(j)`
26
+ - `.dict()` → `.model_dump()`, `.json()` → `.model_dump_json()`
27
+ - `.schema()` → `.model_json_schema()`, `.copy()` → `.model_copy()`
28
+
29
+ ### FastAPI (>=3.9)
30
+
31
+ - `@app.on_event("startup")`/`"shutdown"` → `@asynccontextmanager` lifespan + `FastAPI(lifespan=lifespan)`; yield dict becomes `request.state`
32
+ - `db: Session = Depends(get_db)` → `DbDep = Annotated[Session, Depends(get_db)]`; reusable type alias per PEP 593
33
+
34
+ ### httpx
35
+
36
+ - Per-request `async with httpx.AsyncClient()` → shared `AsyncClient` with `base_url`
37
+ - Caveat: shared client must be closed via `async with` or lifespan management
38
+
39
+ ### asyncio (>=3.11)
40
+
41
+ - `await asyncio.gather(a(), b())` → `async with asyncio.TaskGroup() as tg:` + `tg.create_task()`; access results via `task.result()`
42
+ - Caveat: 3.11+ only. `TaskGroup` cancels siblings on error and raises `ExceptionGroup`; `gather` preserves return order and supports `return_exceptions=True`
43
+
44
+ ### SQLAlchemy 2.0 (>=3.9)
45
+
46
+ - `session.query(User).filter()` → `session.execute(select(User).where())`; use `session.scalars()` for ORM results
47
+ - `Column(Integer)` → `Mapped[int] = mapped_column()`; type inferred from annotation, nullability from `Optional`/`| None`
48
+ - Sync `Session` with `asyncio.to_thread` → `AsyncSession` + `create_async_engine` + `async_sessionmaker`
49
+
50
+ ### Toolchain
51
+
52
+ - `setup.py` / `setup.cfg` → `pyproject.toml` with `[build-system]` + `[project]` (PEP 621)
53
+ - `subprocess.run(f"cmd {arg}", shell=True)` → `subprocess.run(["cmd", arg], check=True)`
54
+ - Caveat: `shell=True` is valid when pipes/globs are needed; use `shlex.quote()` for user input
55
+
56
+ ## All 41 guides by category
57
+
58
+ - **typing** (7): `use-builtin-generics`, `union-syntax`, `type-parameter-syntax`, `override-decorator`, `typeis-vs-typeguard`, `paramspec-decorators`, `deferred-annotations`
59
+ - **async** (3): `taskgroup-over-gather`, `exception-groups`, `async-timeout-context`
60
+ - **stdlib** (5): `datetime-utc`, `pathlib-over-os-path`, `tomllib-builtin`, `removeprefix-removesuffix`, `template-strings`
61
+ - **data-structures** (3): `dict-merge-operator`, `match-case-patterns`, `dataclass-modern`
62
+ - **pydantic** (4): `pydantic-v2-validators`, `pydantic-v2-config`, `pydantic-v2-model-api`, `pydantic-v2-serialization`
63
+ - **fastapi** (3): `fastapi-lifespan`, `fastapi-annotated-depends`, `fastapi-typed-state`
64
+ - **httpx** (2): `httpx-async-client-reuse`, `httpx-streaming`
65
+ - **django** (3): `django-json-field`, `django-async-views`, `django-check-constraints`
66
+ - **sqlalchemy** (3): `sqlalchemy-2-style`, `sqlalchemy-mapped-column`, `sqlalchemy-async-session`
67
+ - **pytest** (3): `pytest-parametrize`, `pytest-tmp-path`, `pytest-raises-match`
68
+ - **toolchain** (5): `pyproject-toml-over-setup`, `uv-over-pip`, `ruff-over-flake8`, `no-pickle`, `safe-subprocess`
69
+
70
+ For full code examples, use `mpg retrieve <guide-id>` or MCP tool `retrieve_guides`.
@@ -1,3 +1,3 @@
1
1
  """Modern Python Guidance — version-aware BAD/GOOD pattern guides for AI coding agents."""
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.3.6"
@@ -74,10 +74,12 @@ def main(argv: list[str] | None = None) -> None:
74
74
  # setup
75
75
  p_setup = subparsers.add_parser(
76
76
  "setup",
77
- help="Register MCP server and link Agent Skills",
77
+ help="Register MCP server and link Agent Skills + Rules",
78
78
  )
79
79
  p_setup.add_argument("--mcp-only", action="store_true", help="MCP registration only")
80
- p_setup.add_argument("--skills-only", action="store_true", help="Skills symlink only")
80
+ p_setup.add_argument(
81
+ "--skills-only", action="store_true", help="Project-local artifacts only (Skills + Rules)"
82
+ )
81
83
  p_setup.add_argument(
82
84
  "--scope",
83
85
  choices=["user", "local"],
@@ -87,21 +89,25 @@ def main(argv: list[str] | None = None) -> None:
87
89
  p_setup.add_argument(
88
90
  "--project-dir",
89
91
  type=Path,
90
- help="Project directory for Skills symlink",
92
+ help="Project directory for Skills/Rules symlinks",
91
93
  )
92
94
  p_setup.add_argument("--dry-run", action="store_true", help="Show what would be done")
93
95
 
94
96
  # uninstall
95
97
  p_uninstall = subparsers.add_parser(
96
98
  "uninstall",
97
- help="Reverse 'setup': deregister MCP server and unlink Agent Skills",
99
+ help="Reverse 'setup': deregister MCP server and unlink Agent Skills + Rules",
98
100
  )
99
101
  p_uninstall.add_argument("--mcp-only", action="store_true", help="MCP deregistration only")
100
- p_uninstall.add_argument("--skills-only", action="store_true", help="Skills unlink only")
102
+ p_uninstall.add_argument(
103
+ "--skills-only",
104
+ action="store_true",
105
+ help="Project-local artifacts only (Skills + Rules)",
106
+ )
101
107
  p_uninstall.add_argument(
102
108
  "--project-dir",
103
109
  type=Path,
104
- help="Project directory for Skills symlink",
110
+ help="Project directory for Skills/Rules symlinks",
105
111
  )
106
112
  p_uninstall.add_argument("--dry-run", action="store_true", help="Show what would be done")
107
113
 
@@ -11,8 +11,16 @@ import sys
11
11
  from pathlib import Path
12
12
 
13
13
  SKILLS_LINK_NAME = "modern-python-guidance"
14
+ RULE_FILE_NAME = "modern-python.md"
14
15
  MCP_SERVER_NAME = "mpg"
15
16
 
17
+ RULE_FRONTMATTER = (
18
+ "---\n"
19
+ 'paths: ["**/*.py", "*.py", "**/pyproject.toml", "**/requirements*.txt",'
20
+ ' "**/setup.py", "**/setup.cfg", "**/.python-version", "**/Pipfile"]\n'
21
+ "---"
22
+ )
23
+
16
24
 
17
25
  def _find_skills_dir() -> Path:
18
26
  """Resolve the bundled skills directory (package install or editable)."""
@@ -33,6 +41,25 @@ def _find_skills_dir() -> Path:
33
41
  raise FileNotFoundError(msg)
34
42
 
35
43
 
44
+ def _find_rule_source() -> Path:
45
+ """Resolve the bundled rule file (package install or editable)."""
46
+ try:
47
+ pkg = importlib.resources.files("modern_python_guidance") / "rules"
48
+ rule_path = Path(str(pkg)) / RULE_FILE_NAME
49
+ if rule_path.is_file():
50
+ return rule_path
51
+ except (TypeError, FileNotFoundError):
52
+ pass
53
+
54
+ src_root = Path(__file__).resolve().parent.parent.parent
55
+ dev_path = src_root / "rules" / RULE_FILE_NAME
56
+ if dev_path.is_file():
57
+ return dev_path
58
+
59
+ msg = "Cannot locate bundled rule file"
60
+ raise FileNotFoundError(msg)
61
+
62
+
36
63
  def _find_project_root(start: Path | None = None) -> Path:
37
64
  """Walk upward from *start* to find the project root."""
38
65
  current = (start or Path.cwd()).resolve()
@@ -63,6 +90,26 @@ def _skills_link_path(project_dir: Path | None = None) -> Path:
63
90
  return root / ".claude" / "skills" / SKILLS_LINK_NAME
64
91
 
65
92
 
93
+ def _rules_file_path(project_dir: Path | None = None) -> Path:
94
+ """Resolve the rule file symlink path: ``<root>/.claude/rules/modern-python.md``."""
95
+ root = project_dir or _find_project_root()
96
+ return root / ".claude" / "rules" / RULE_FILE_NAME
97
+
98
+
99
+ def _build_rule_text() -> str:
100
+ """Generate rule file content from SKILL.md body with rule-specific frontmatter.
101
+
102
+ Used by CI sync tests to verify the bundled ``rules/modern-python.md`` matches
103
+ what would be generated from SKILL.md. Strips SKILL.md frontmatter and prepends
104
+ rule-only frontmatter (no name/description keys).
105
+ """
106
+ skills_dir = _find_skills_dir()
107
+ skill_md = (skills_dir / "SKILL.md").read_text(encoding="utf-8")
108
+ parts = skill_md.split("---", 2)
109
+ body = parts[2].lstrip("\n")
110
+ return RULE_FRONTMATTER + "\n\n" + body
111
+
112
+
66
113
  def setup_mcp(
67
114
  *,
68
115
  scope: str = "user",
@@ -80,7 +127,7 @@ def setup_mcp(
80
127
  print("Error: 'claude' command not found.", file=sys.stderr)
81
128
  print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
82
129
  print(
83
- "Run 'mpg setup --skills-only' to set up Agent Skills without MCP.",
130
+ "Run 'mpg setup --skills-only' to set up project-local artifacts without MCP.",
84
131
  file=sys.stderr,
85
132
  )
86
133
  return False
@@ -156,6 +203,58 @@ def setup_skills(
156
203
  return True
157
204
 
158
205
 
206
+ def setup_rules(
207
+ *,
208
+ project_dir: Path | None = None,
209
+ dry_run: bool = False,
210
+ ) -> bool:
211
+ """Create a rule file symlink. Returns True on success."""
212
+ try:
213
+ source = _find_rule_source()
214
+ except FileNotFoundError as e:
215
+ print(f"Error: {e}", file=sys.stderr)
216
+ return False
217
+
218
+ if source.is_symlink():
219
+ print("Error: rule source is itself a symlink (unexpected).", file=sys.stderr)
220
+ return False
221
+
222
+ root = project_dir or _find_project_root()
223
+ link_path = _rules_file_path(project_dir)
224
+ rules_parent = link_path.parent
225
+
226
+ if dry_run:
227
+ print(f"Would link: {link_path} -> {source}")
228
+ return True
229
+
230
+ if link_path.is_symlink():
231
+ current_target = Path(os.readlink(link_path))
232
+ if current_target == source or link_path.resolve() == source.resolve():
233
+ print(f"Rule already linked at {link_path.relative_to(root)}")
234
+ return True
235
+ link_path.unlink()
236
+ elif link_path.exists():
237
+ print(
238
+ f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
239
+ file=sys.stderr,
240
+ )
241
+ print(
242
+ f"Remove it manually: rm {shlex.quote(str(link_path))}",
243
+ file=sys.stderr,
244
+ )
245
+ return False
246
+
247
+ try:
248
+ rules_parent.mkdir(parents=True, exist_ok=True)
249
+ os.symlink(source, link_path)
250
+ except OSError as e:
251
+ print(f"Error creating symlink: {e}", file=sys.stderr)
252
+ return False
253
+
254
+ print(f"Rule linked to {link_path.relative_to(root)}")
255
+ return True
256
+
257
+
159
258
  def run_setup(
160
259
  *,
161
260
  scope: str = "user",
@@ -171,9 +270,11 @@ def run_setup(
171
270
 
172
271
  do_mcp = not skills_only
173
272
  do_skills = not mcp_only
273
+ do_rules = not mcp_only
174
274
 
175
275
  mcp_ok = True
176
276
  skills_ok = True
277
+ rules_ok = True
177
278
 
178
279
  if do_mcp:
179
280
  mcp_ok = setup_mcp(scope=scope, dry_run=dry_run)
@@ -181,7 +282,10 @@ def run_setup(
181
282
  if do_skills:
182
283
  skills_ok = setup_skills(project_dir=project_dir, dry_run=dry_run)
183
284
 
184
- if mcp_ok and skills_ok:
285
+ if do_rules:
286
+ rules_ok = setup_rules(project_dir=project_dir, dry_run=dry_run)
287
+
288
+ if mcp_ok and skills_ok and rules_ok:
185
289
  if not dry_run and do_mcp and do_skills:
186
290
  print("Ready. Start Claude Code to use mpg guides.")
187
291
  return 0
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  from modern_python_guidance.setup_cmd import (
12
12
  MCP_SERVER_NAME,
13
13
  _find_project_root,
14
+ _rules_file_path,
14
15
  _skills_link_path,
15
16
  )
16
17
 
@@ -48,7 +49,7 @@ def uninstall_mcp(*, dry_run: bool = False) -> bool:
48
49
  print("Error: 'claude' command not found.", file=sys.stderr)
49
50
  print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
50
51
  print(
51
- "Run 'mpg uninstall --skills-only' to remove Agent Skills without MCP.",
52
+ "Run 'mpg uninstall --skills-only' to remove project-local artifacts without MCP.",
52
53
  file=sys.stderr,
53
54
  )
54
55
  return False
@@ -139,6 +140,47 @@ def uninstall_skills(
139
140
  return True
140
141
 
141
142
 
143
+ def uninstall_rules(
144
+ *,
145
+ project_dir: Path | None = None,
146
+ dry_run: bool = False,
147
+ ) -> bool:
148
+ """Remove the rule file symlink. Returns True on success.
149
+
150
+ Only a symlink is removed. A non-symlink entity at the path is refused.
151
+ Idempotent: if no symlink is present, this is a no-op success.
152
+ """
153
+ root = project_dir or _find_project_root()
154
+ link_path = _rules_file_path(project_dir)
155
+
156
+ if not link_path.is_symlink():
157
+ if link_path.exists():
158
+ print(
159
+ f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
160
+ file=sys.stderr,
161
+ )
162
+ print(
163
+ f"Remove it manually: rm {shlex.quote(str(link_path))}",
164
+ file=sys.stderr,
165
+ )
166
+ return False
167
+ print(f"Rule not linked at {link_path.relative_to(root)} — nothing to remove.")
168
+ return True
169
+
170
+ if dry_run:
171
+ print(f"Would remove: {link_path}")
172
+ return True
173
+
174
+ try:
175
+ link_path.unlink()
176
+ except OSError as e:
177
+ print(f"Error removing symlink: {e}", file=sys.stderr)
178
+ return False
179
+
180
+ print(f"Rule unlinked from {link_path.relative_to(root)}")
181
+ return True
182
+
183
+
142
184
  def run_uninstall(
143
185
  *,
144
186
  mcp_only: bool = False,
@@ -153,9 +195,11 @@ def run_uninstall(
153
195
 
154
196
  do_mcp = not skills_only
155
197
  do_skills = not mcp_only
198
+ do_rules = not mcp_only
156
199
 
157
200
  mcp_ok = True
158
201
  skills_ok = True
202
+ rules_ok = True
159
203
 
160
204
  if do_mcp:
161
205
  mcp_ok = uninstall_mcp(dry_run=dry_run)
@@ -163,7 +207,10 @@ def run_uninstall(
163
207
  if do_skills:
164
208
  skills_ok = uninstall_skills(project_dir=project_dir, dry_run=dry_run)
165
209
 
166
- if mcp_ok and skills_ok:
210
+ if do_rules:
211
+ rules_ok = uninstall_rules(project_dir=project_dir, dry_run=dry_run)
212
+
213
+ if mcp_ok and skills_ok and rules_ok:
167
214
  if not dry_run and do_mcp and do_skills:
168
215
  print("Done. mpg has been removed.")
169
216
  return 0
@@ -18,6 +18,7 @@ import pytest
18
18
 
19
19
  from modern_python_guidance.frontmatter import parse_frontmatter
20
20
  from modern_python_guidance.guide_index import _find_guides_dir
21
+ from modern_python_guidance.setup_cmd import _build_rule_text
21
22
 
22
23
  GUIDES_DIR = _find_guides_dir()
23
24
  EXPECTED_GUIDE_COUNT = 41
@@ -112,6 +113,56 @@ class TestGuideStructure:
112
113
  assert body.startswith("# "), "body does not start with H1 heading"
113
114
 
114
115
 
116
+ class TestRuleFileSync:
117
+ """CI sync tests: rules/modern-python.md body matches SKILL.md body."""
118
+
119
+ def _skill_body(self) -> str:
120
+ skill_md = (GUIDES_DIR.parent / "SKILL.md").read_text(encoding="utf-8")
121
+ parts = skill_md.split("---", 2)
122
+ return parts[2].lstrip("\n")
123
+
124
+ def _rule_path(self) -> Path:
125
+ return GUIDES_DIR.parent.parent.parent / "rules" / "modern-python.md"
126
+
127
+ def _rule_parts(self) -> tuple[str, str]:
128
+ text = self._rule_path().read_text(encoding="utf-8")
129
+ parts = text.split("---", 2)
130
+ return parts[1].strip(), parts[2].lstrip("\n")
131
+
132
+ def test_body_matches_skill(self):
133
+ """rules/modern-python.md body == SKILL.md body (content sync)."""
134
+ skill_body = self._skill_body()
135
+ _, rule_body = self._rule_parts()
136
+ assert rule_body == skill_body
137
+
138
+ def test_matches_build_rule_text(self):
139
+ """rules/modern-python.md == _build_rule_text() output (SoT enforcement)."""
140
+ actual = self._rule_path().read_text(encoding="utf-8")
141
+ expected = _build_rule_text()
142
+ assert actual == expected
143
+
144
+ def test_frontmatter_has_paths(self):
145
+ """rules/modern-python.md frontmatter contains expected paths patterns."""
146
+ fm, _ = self._rule_parts()
147
+ for pattern in [
148
+ "**/*.py",
149
+ "*.py",
150
+ "**/pyproject.toml",
151
+ "**/requirements*.txt",
152
+ "**/setup.py",
153
+ "**/setup.cfg",
154
+ "**/.python-version",
155
+ "**/Pipfile",
156
+ ]:
157
+ assert pattern in fm, f"missing path pattern: {pattern}"
158
+
159
+ def test_frontmatter_no_name_or_description(self):
160
+ """rules/modern-python.md frontmatter has NO name/description keys."""
161
+ fm, _ = self._rule_parts()
162
+ assert "name:" not in fm
163
+ assert "description:" not in fm
164
+
165
+
115
166
  class TestGuideInventory:
116
167
  def test_no_duplicate_ids(self):
117
168
  seen: dict[str, Path] = {}