- name
- repomatic-deps
- description
- Generate dependency graphs. Audit pyproject.toml declarations against the version policy. Explore unused dependency APIs that could simplify code. Modernize code against the changelogs of upgraded dependencies.
- compatibility
- Designed for Claude Code. Recommended model: Opus.
- allowed-tools
- Bash Read Grep Glob Agent Edit Write WebFetch
- argument-hint
- [graph [--level N]|review [all|runtime|dev|policy]|explore [<package>]|modernize [<package>]]
## Context
!`[ -f uv.lock ] && echo "uv.lock exists" || echo "No uv.lock found"`
!`[ -f pyproject.toml ] && head -5 pyproject.toml || echo "No pyproject.toml found"`
!`grep -c '".*>=\|".*~=\|".*<\|".*==' pyproject.toml 2>/dev/null || echo "0"`
!`[ -f repomatic/__init__.py ] && echo "CANONICAL_REPO" || echo "DOWNSTREAM"`
## Instructions
You help users understand and maintain their project's dependencies. This skill has four modes: **graph** (visualize the resolved dependency tree), **review** (audit `pyproject.toml` declarations against version policy), **explore** (find unused dependency APIs that could simplify existing code), and **modernize** (refactor code to adopt features from recently-upgraded dependencies, applying the changes).
### Determine invocation method
- If the context above shows `CANONICAL_REPO`, use `uv run repomatic`.
- Otherwise, use `uvx -- repomatic`.
- Gate the `uvx` form with the supply-chain cooldown: `uvx --exclude-newer '1 week' --exclude-newer-package repomatic=P0D -- repomatic`. The window matches `[tool.repomatic] minimum-release-age`; repomatic itself is exempt because a fresh release must stay installable, while its dependency tree stays gated.
### Mode selection
- No arguments: Run both `graph` and `review all`.
- `graph`: Generate and analyze the dependency graph only.
- `review [all|runtime|dev|policy]`: Audit `pyproject.toml` declarations only.
- `explore [<package>]`: Search for unused dependency APIs that could simplify existing code.
- `modernize [<package>]`: Refactor code to adopt new features from upgraded dependencies, applying and test-gating each change.
- If `$ARGUMENTS` starts with `--level` or a number, treat it as `graph` mode with those arguments.
---
## Graph mode
### Mechanical layer
The `autofix.yaml` workflow's `update-dep-graph` job regenerates the dependency graph on every push, for packages and uv virtual projects alike. This mode is useful for **interactive analysis** — understanding the graph, spotting concerns, or generating it before pushing.
### Argument handling
- Pass remaining arguments through to `<cmd> update-dep-graph`.
- If no extra arguments, run `<cmd> update-dep-graph` with no arguments.
### After running
- Display the Mermaid output.
- Analyze the graph: count total dependencies, flag deep dependency chains, identify packages with high fan-in (many dependents) or fan-out (many dependencies).
- Highlight any notable patterns or potential concerns (e.g., single points of failure, overly deep transitive chains).
---
## Review mode
### Mechanical layer
`<cmd> lint-deps` decides everything readable from `pyproject.toml` alone: upper bounds, missing specifiers, unsorted lists, type stubs outside the `typing` group, floors with no comment above them, and floor comments running past `[tool.repomatic] lint-deps.comment-word-threshold` words (40 by default). Run it first and let it own those rows. CI runs it from the release lane's `lint-deps` job (`_release-build.yaml`, reached through `release.yaml`), which fires on every push but annotates with `--no-fatal` until a release commit makes the *shippability* findings fatal. These policy rows never block either way, so no CI gate will catch them for you.
What is left is the part no parser settles, and it is the reason this mode exists: whether a floor is **justified** by the APIs the code actually calls, whether a comment is **stale or weak**, and whether a rationale **contradicts** its conditional marker. Spend the review there.
### Scope selection
- `all` (default when no sub-argument): Run all checks below.
- `runtime`: Review `[project].dependencies` only.
- `dev`: Review `[dependency-groups]` and `[project.optional-dependencies]` only.
- `policy`: Print the policy summary without auditing.
### Version specifier policy
These conventions are derived from the `pyproject.toml` files across all `kdeldycke/*` repositories. User-facing documentation of the same content is in [`docs/dependencies.md`](https://repomatic.net/dependencies).
#### Runtime dependencies (`[project].dependencies`)
1. **Use `>=` (not `~=` or `==`).** Relaxed lower bounds give packagers freedom to release security hotfixes without waiting for an upstream bump. Upper bounds are forbidden per [https://iscinumpy.dev/post/bound-version-constraints/](https://iscinumpy.dev/post/bound-version-constraints/).
2. **Every version bound needs a comment tying the floor to a concrete code dependency.** The comment goes on the line above the dependency and states which feature, method, or API from that version the project actually uses. Prefer referencing the call site or module that depends on it:
```toml
# wcmatch 10.0 changed globbing semantics; our sync_gitignore() relies on
# the new symlink-aware matching behavior.
"wcmatch>=10",
```
A good floor comment answers: "if someone installed an older version, what would break and where?" If you cannot point to a concrete usage, the floor may be unnecessarily high.
**A floor comment documents the floor as it stands, in one short paragraph.** It is not a record of how the floor got there. Left alone it drifts that way on its own: each bump appends a paragraph about the newly required version, nothing is deleted, and the comment becomes a private changelog of the dependency with the declared version buried under the floors it replaced. When raising a floor, **rewrite the comment rather than extending it**: keep what the new version buys (API, fix, `requires-python` alignment, the call site consuming it, a CVE or upstream issue identifier), delete every superseded floor (`git log -- pyproject.toml` keeps that history), and move out what is not about this floor (usage detail belongs in the module using it, a comparison against an alternative package in an `XXX` pointer to the upstream ticket). `lint-deps` warns past 40 words, which is the ceiling to write to even where it is not run.
**Security fixes are also a valid floor bump reason.** A CVE or advisory in an older version justifies raising the floor even when the API is unchanged. The comment should cite the CVE or advisory:
```toml
# requests 2.32.0 fixes CVE-2024-35195 (session credential leak on redirects).
"requests>=2.32",
```
3. **Python version support is not a valid reason to bump a floor.** The dependency resolver already picks the right version via `requires-python` metadata. If `boltons>=20` works and boltons 25 merely adds Python 3.13 support, keep `>=20` — the resolver handles it. **Exception:** when a dependency *drops* a Python version your project still supports (or your project drops one, aligning minimum `requires-python`), that alignment is a valid floor bump reason. The comment should state the version range alignment, not the Python support:
```toml
# boltons 25.0.0 dropped Python 3.9, matching our requires-python >= 3.10.
"boltons>=25",
```
4. **Use conditional markers for Python-version-gated deps.** Example: `"tomli>=2; python_version<'3.11'"`. When a dep has a version marker, the floor rationale must make sense for the Python versions where the dep is actually installed — not for versions excluded by the marker.
5. **Alphabetical order** within the list.
#### Development dependencies (`[dependency-groups]`)
06. **Prefer `[dependency-groups]`** (uv standard) over `[project.optional-dependencies]` for test, typing, and docs groups.
07. **`>=` is preferred for dev deps too**, but `~=` is acceptable when stricter pinning reduces CI randomness. If a package also appears in runtime deps, the dev entry must use the same specifier style. The relaxation is about specifier *style* (`~=` allowed), not about floor *accuracy* — dev dep floors still need to be grounded in actual API or compatibility requirements, not adoption timestamps.
08. **Standard group names:** `test`, `typing`, `docs` (lowercase, alphabetical).
09. **Type stubs** go in the `typing` group with stub-specific versions: `"types-boltons>=25.0.0.20250822"`.
10. **Alphabetical order** within each group.
#### General rules
11. **No upper bounds** (`<`, `<=`, `!=`, `~=` that implies an upper bound). The only exception is conditional markers like `python_version<'3.11'`.
12. **Extras syntax** is fine: `"coverage[toml]>=7.11"`.
13. **One dependency per line** for readable diffs. Short groups that fit on one line are acceptable — the `format-pyproject` job normalizes layout automatically.
### Audit procedure
Read the full `pyproject.toml`. `lint-deps` already reports the specifier style, missing comments, ordering, bare dependencies and misplaced type stubs, so read its output rather than re-deriving them. For each dependency entry, check what it cannot:
| Check | What to flag |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Weak comment | Comment cites Python version support instead of a concrete code dependency. Flag unless it documents a `requires-python` alignment or a Python version drop |
| Stale comment | Comment references a reason that no longer applies (the cited method was replaced, or the Python version left the support matrix) |
| Accreted comment | Comment narrates superseded floors beside the declared one. Rewrite it around the version in force; `lint-deps` catches only the long ones |
| Inflated floor | Floor higher than the oldest version providing the APIs actually used (see [floor verification](#floor-verification) below) |
| Marker/rationale mismatch | Floor rationale contradicts the conditional marker (e.g., "Python 3.14 wheels" on a dep gated by `python_version<'3.11'` — that dep is never installed on 3.14) |
| Section style | `[project.optional-dependencies]` used where `[dependency-groups]` would be appropriate |
| Conditional markers | Missing Python version marker for backport packages |
| Stale cooldown exceptions | `exclude-newer-package` entries in `[tool.uv]` for packages that no longer need them (see below) |
### Floor verification
Comments and changelogs can lie; the codebase is the source of truth. For each dependency with a weak or suspicious comment, verify the floor against actual usage:
1. **Grep for imports.** Search the source tree for all imports from the package. List the specific APIs used (functions, classes, constants).
2. **Determine the oldest version providing those APIs.** Check when the API was introduced — changelogs, release notes, or `pip index versions <pkg>` to see what exists on PyPI. A floor that rests on a behavior (comments kept, output formatting) needs a bisect instead, since a changelog can credit it to a later release than the one that shipped it. Run one probe per release with `uv run --no-project --exclude-newer '1 week' --with '<pkg>==<version>' python probe.py`, and compare each output byte for byte with the locked release.
3. **Lower the floor** when it exceeds the oldest compatible version. Prefer conservative minimums (the major version that introduced the API) over aggressive ones. Update both the version specifier and the comment.
4. **Run `uv lock`** after any floor change to verify the lock still resolves.
#### Special cases
- **Backport packages** (like `backports-strenum`, `tomli`, `exceptiongroup`) exist solely to provide a stdlib class to older Python versions. Their entire API is the backported class itself, available in all versions. The floor is typically `>=1` (or the first release) unless a specific bug fix is needed for the Python versions where the dep is actually installed.
- **Conditional deps with stale bug-fix floors.** A dep gated by `python_version<'3.11'` that has a floor set for a bug affecting Python \<3.8.6 — if the project's `requires-python` is `>=3.10`, that bug is irrelevant and the floor can be lowered.
- **pytest plugins** with no special API beyond auto-registration (like `pytest-randomly`, `pytest-github-actions-annotate-failures`) have low effective floors — their basic functionality has been stable across major versions. Set the floor at the major version introducing the current plugin interface, not at the latest release.
#### Floor bumps to adopt new APIs
A floor bump is justified when a newer version of an existing dependency provides an API that **replaces hand-rolled code** in the project. This is the flip side of floor verification: instead of checking whether the floor is too high, check whether it could be raised to unlock a simplification.
A valid simplification bump must:
1. **Replace existing code**, not add new features. The goal is less code, not more capability.
2. **Be a net reduction** in complexity. Swapping a one-line comprehension for a library call is not a win.
3. **Use the public API** of the dependency. Private/undocumented attributes do not count.
4. **Update the floor comment** to reference the new API and the code it replaces.
When `explore` mode identifies a candidate, the review output should include it as an **Info**-level suggestion with the current code, the replacement, and the version that introduced the API.
#### Red flag patterns in comments
These comment patterns typically signal a floor set at adoption or auto-bump time, not at an API boundary:
- "First version we used" / "first version when we last changed the requirement" — the floor is an artifact of when the dep was added or last bumped by a dependency bot, not a deliberate API minimum.
- "First version to support Python 3.X" — unless it documents a `requires-python` drop alignment or a concrete build failure (missing wheels that cause install failures on that Python version), this is not a valid floor reason.
- **The `~= -> >=` conversion pipeline.** A common inflation path: (a) dep added as `~=X.Y` (latest at time), (b) a dependency bot bumps to `~=X.Z`, (c) a bulk "relax requirements" commit converts all `~=` to `>=`. Each step inflates the floor without API validation. Check `git log` for this pattern when a floor looks suspiciously high.
### `exclude-newer-package` cooldown audit
The `[tool.uv]` section may contain `exclude-newer-package` entries that exempt specific packages from the global `exclude-newer` cooldown window. They arrive from two places: written by hand (the package is published by the same maintainer, or is developed in-repo), or written by `audit --fix`, which reaches a CVE fix still inside the window through an entry rather than lifting `exclude-newer` for the whole tree.
**Expiry is mechanical, so do not audit for it.** `sync-uv-lock` owns the lifecycle of every entry and reports each move in its PR body: it rewrites a relative span (`"0 day"`) into a fixed cutoff pinned to the locked version's upload time, which *holds* the package instead of letting it track latest, then prunes the entry outright once that held version ages past the global cooldown and the package rejoins normal resolution. A live fixed cutoff is therefore a freeze doing its job, not a leftover: proposing its deletion un-holds a package the freeze was deliberately pinning. A surviving relative span means the freeze has not run yet, not that a span is the steady state.
What is left for this audit is the part no schedule settles:
1. **Is the package still a dependency?** If it was removed from `[project].dependencies` and all `[dependency-groups]`, the entry is dead weight that no prune will ever reach, since pruning keys off the locked version's age and the package is no longer in the lock.
2. **Is a hand-written exemption still justified?** A same-maintainer or in-repo package keeps its span indefinitely and correctly. An external package exempted during a migration that has since finished no longer needs one.
3. **Does the comment explain the reason?** Like version floors, a hand-written exemption should carry a comment naming why the cooldown does not apply to it.
Flag those as warnings. Never flag an entry merely for existing or for looking old.
### Cross-repo reference
When the context shows `DOWNSTREAM`, also compare the dependency list against the canonical `repomatic` `pyproject.toml`, fetched at the version this repo has adopted rather than at the tip of `main`: take the tag from the `uses:` pins in `.github/workflows/`, then run `gh api "repos/kdeldycke/repomatic/contents/pyproject.toml?ref=vX.Y.Z" --jq '.content' | base64 -d`. An unpinned fetch resolves to `main`, whose floors may have moved for a release the downstream repo cannot use yet, turning unreleased work into a phantom "downstream is behind" finding. Use it to identify:
- Shared dependencies where the downstream floor is lower than upstream (may be missing a needed bump).
- Shared dev dependencies where upstream has moved to a newer group structure.
### Output format
Produce:
1. **Policy compliance summary**: A table with one row per dependency: name, specifier, has comment (yes/no), issues found.
2. **Grouped findings** by severity:
- **Errors**: Wrong specifier style, missing version, upper bounds.
- **Warnings**: Missing or stale comments, ordering issues, inflated floors, marker/rationale mismatches.
- **Info**: Suggestions for floor adjustments based on API verification or cross-repo data.
3. **Suggested fixes**: For each error/warning, show the current line and the recommended replacement. For inflated floors, include the verified API minimum and a rewritten comment.
---
## Explore mode
Search for unused APIs in existing dependencies that could replace hand-rolled code. This is purely analytical: it produces recommendations, not changes.
### Scope selection
- No argument: explore all runtime dependencies.
- `<package>`: explore a single dependency (e.g., `explore boltons`).
### Procedure
For each dependency in scope:
1. **Catalog current usage.** Grep the source tree (not tests) for all imports from the package. List every function, class, and constant actually used, with file locations.
2. **Catalog available APIs.** Using your knowledge of the library (and its docs if needed via `WebFetch`), list the public APIs the project does NOT currently use. Focus on utilities, helpers, and data structures: the kind of thing that replaces 3-10 lines of hand-rolled code.
3. **Search for replacement candidates.** For each unused API, grep the source tree for code patterns it could replace. Be specific about what constitutes a match:
| Library API | Pattern to search for |
View on GitHub