Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ elif echo "$MATCH_CMD" | grep -qE '^pip[[:space:]]+(list|outdated|install|show)(
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^uv[[:space:]]+pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^uv pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^mypy/rtk mypy/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m mypy/rtk mypy/')"

# --- Go tooling ---
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+test([[:space:]]|$)'; then
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ rtk gain --history | grep proxy
| pnpm_cmd.rs | pnpm package manager | Compact dependency trees (70-90% reduction) |
| ruff_cmd.rs | Ruff linter/formatter | JSON for check, text for format (80%+ reduction) |
| pytest_cmd.rs | Pytest test runner | State machine text parser (90%+ reduction) |
| mypy_cmd.rs | Mypy type checker | Group by file/error code (80% reduction) |
| pip_cmd.rs | pip/uv package manager | JSON parsing, auto-detect uv (70-85% reduction) |
| go_cmd.rs | Go commands | NDJSON for test, text for build/vet (80-90% reduction) |
| golangci_cmd.rs | golangci-lint | JSON parsing, group by rule (85% reduction) |
Expand Down Expand Up @@ -312,3 +313,10 @@ GitHub Actions workflow (.github/workflows/release.yml):
- DEB/RPM package generation
- Automated releases on version tags (v*)
- Checksums for binary verification

## Active Technologies
- Rust 2021 edition + regex (1), lazy_static (1.4), anyhow (1.0) -- all already in Cargo.toml (001-mypy-cmd)
- SQLite via rusqlite (existing tracking.rs) (001-mypy-cmd)

## Recent Changes
- 001-mypy-cmd: Added Rust 2021 edition + regex (1), lazy_static (1.4), anyhow (1.0) -- all already in Cargo.toml
4 changes: 4 additions & 0 deletions hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ elif echo "$MATCH_CMD" | grep -qE '^pip[[:space:]]+(list|outdated|install|show)(
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^uv[[:space:]]+pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^uv pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^mypy/rtk mypy/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m mypy/rtk mypy/')"

# --- Go tooling ---
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+test([[:space:]]|$)'; then
Expand Down
37 changes: 37 additions & 0 deletions specs/001-mypy-cmd/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Specification Quality Checklist: RTK Mypy Command

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-13
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- All items pass. Spec is ready for `/speckit.plan`.
- FR-001 through FR-009 cover the core filter function (testable with unit tests on raw strings).
- FR-010 through FR-013 cover the command execution wrapper (testable with integration patterns).
- FR-014 and FR-015 cover discovery and hook integration (testable with registry unit tests and hook script assertions).
203 changes: 203 additions & 0 deletions specs/001-mypy-cmd/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Implementation Plan: RTK Mypy Command

**Branch**: `001-mypy-cmd` | **Date**: 2026-02-13 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/001-mypy-cmd/spec.md`

## Summary

Add `rtk mypy` command that filters and compresses mypy type-checker output by parsing errors, grouping by file (most errors first), displaying a summary header with total counts and top error codes, and preserving every individual error. Follows the exact same structural pattern as `tsc_cmd.rs`. Includes discover registry pattern, auto-rewrite hook entry, and comprehensive TDD tests.

## Technical Context

**Language/Version**: Rust 2021 edition
**Primary Dependencies**: regex (1), lazy_static (1.4), anyhow (1.0) -- all already in Cargo.toml
**Storage**: SQLite via rusqlite (existing tracking.rs)
**Testing**: cargo test with embedded `#[cfg(test)] mod tests`
**Target Platform**: macOS, Linux (cross-platform CLI)
**Project Type**: Single Rust binary (existing)
**Performance Goals**: N/A (output filtering is string processing on small inputs)
**Constraints**: No new dependencies. Must follow existing module patterns exactly.
**Scale/Scope**: 6 files modified/created total

## Constitution Check

*No constitution file exists. Skipping gate check.*

## Project Structure

### Documentation (this feature)

```text
specs/001-mypy-cmd/
├── spec.md
├── plan.md # This file
├── research.md # Phase 0 output
├── checklists/
│ └── requirements.md # Already created
└── tasks.md # Phase 2 output (created by /speckit.tasks)
```

### Source Code (repository root)

```text
src/
├── mypy_cmd.rs # NEW: mypy command module (filter + run)
├── main.rs # MODIFY: add Mypy variant to Commands enum + match arm
├── discover/
│ └── registry.rs # MODIFY: add mypy + python3 -m mypy patterns and rules

.claude/hooks/
└── rtk-rewrite.sh # MODIFY: add mypy rewrite patterns

hooks/
└── rtk-rewrite.sh # MODIFY: mirror of .claude/hooks/rtk-rewrite.sh
```

**Structure Decision**: This is a leaf module addition following the exact pattern of `tsc_cmd.rs`, `ruff_cmd.rs`, and `pytest_cmd.rs`. No new directories, no new dependencies, no architectural changes.

## Design

### mypy_cmd.rs -- Core Module

**Pattern**: Mirrors `tsc_cmd.rs` exactly.

**Public API**:
```
pub fn run(args: &[String], verbose: u8) -> Result<()>
```

**Internal filter function** (unit-testable):
```
fn filter_mypy_output(output: &str) -> String
```

**Mypy output format** (the input we parse):
```
src/module.py:12: error: Incompatible return value type [return-value]
src/module.py:12:5: error: Incompatible return value type [return-value]
src/module.py:15: note: Expected "int"
src/other.py:8: error: Name "foo" is not defined [name-defined]
Found 3 errors in 2 files (checked 10 source files)
```

**Key patterns**:
- Error line regex: `^(.+?):(\d+)(?::(\d+))?: (error|warning|note): (.+?)(?:\s+\[(.+)\])?$`
- Continuation: `note:` lines attach to the preceding error
- File-less errors: Lines matching `error:` without a file path prefix (e.g., mypy config errors) -- display verbatim at top
- Summary line: `Found N errors in M files` -- replaced by our header

**RTK output format** (what we produce):
```
mypy: 3 errors in 2 files
=======================================
Top codes: return-value (1x), name-defined (1x)

src/module.py (2 errors)
L12: [return-value] Incompatible return value type
Expected "int"
L15: [some-code] Another error

src/other.py (1 error)
L8: [name-defined] Name "foo" is not defined
```

**Command execution flow**:
1. Try `mypy` directly via `Command::new("mypy")`
2. If not found, try `python3 -m mypy` as fallback (same pattern as pytest_cmd.rs)
3. Forward all user args
4. Capture stdout + stderr, combine
5. Strip ANSI codes via `utils::strip_ansi()`
6. Filter through `filter_mypy_output()`
7. Track via `tracking::TimedExecution`
8. Exit with mypy's exit code via `std::process::exit()`

### main.rs -- Wiring

Add to `Commands` enum (alphabetical placement near other Python tools):
```
Mypy {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
```

Add match arm:
```
Commands::Mypy { args } => {
mypy_cmd::run(&args, cli.verbose)?;
}
```

Add module declaration:
```
mod mypy_cmd;
```

### discover/registry.rs -- Discovery

Add pattern after the existing Python tool patterns (after ruff, before docker):
```
r"^(python3?\s+-m\s+)?mypy(\s|$)"
```

Add corresponding rule:
```
RtkRule {
rtk_cmd: "rtk mypy",
category: "Build",
savings_pct: 80.0,
subcmd_savings: &[],
subcmd_status: &[],
}
```

### rtk-rewrite.sh -- Hook (both locations)

Add after the ruff rewrite block in the "Python tooling" section:
```bash
elif echo "$MATCH_CMD" | grep -qE '^mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^mypy/rtk mypy/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m mypy/rtk mypy/')"
```

## File Change Summary

| File | Action | Lines (est.) | Risk |
|------|--------|-------------|------|
| `src/mypy_cmd.rs` | CREATE | ~200 | Low -- follows tsc_cmd.rs pattern |
| `src/main.rs` | MODIFY | +6 (mod decl, enum variant, match arm) | Low |
| `src/discover/registry.rs` | MODIFY | +10 (pattern + rule + test) | Low |
| `.claude/hooks/rtk-rewrite.sh` | MODIFY | +4 (two elif blocks) | Low |
| `hooks/rtk-rewrite.sh` | MODIFY | +4 (mirror) | Low |

**Total**: 1 new file, 4 modified files. ~224 new lines.

## Testing Strategy

All tests follow TDD (Red-Green-Refactor) per project conventions.

**Unit tests in mypy_cmd.rs** (embedded `#[cfg(test)] mod tests`):

| Test | Validates |
|------|-----------|
| `test_filter_mypy_errors_grouped_by_file` | FR-001, FR-003, FR-004: Multi-file errors grouped correctly |
| `test_filter_mypy_with_column_numbers` | FR-002: Extended format `file:line:col:` parsed |
| `test_filter_mypy_top_codes_summary` | FR-005: Top codes shown when 2+ distinct codes |
| `test_filter_mypy_single_code_no_summary` | FR-005: Top codes omitted with 1 code |
| `test_filter_mypy_every_error_shown` | FR-006: No error messages collapsed |
| `test_filter_mypy_note_continuation` | FR-007: note: lines preserved as context |
| `test_filter_mypy_fileless_errors` | FR-008: Config errors shown verbatim at top |
| `test_filter_mypy_no_errors` | FR-013: Success message for clean output |
| `test_filter_mypy_no_file_limit` | All files shown (mirrors tsc test) |

**Unit tests in discover/registry.rs** (added to existing test module):

| Test | Validates |
|------|-----------|
| `test_classify_mypy` | FR-014: `mypy src/` classified as Supported |
| `test_classify_python_m_mypy` | FR-014: `python3 -m mypy` classified as Supported |

## Complexity Tracking

No constitution violations. No complexity justification needed.
34 changes: 34 additions & 0 deletions specs/001-mypy-cmd/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Research: RTK Mypy Command

**Date**: 2026-02-13
**Status**: Complete

## Findings

### Mypy Output Format Stability

- **Decision**: Parse the standard `file:line: severity: message [code]` format.
- **Rationale**: This format has been stable since mypy 0.9 (2020). The optional column format (`file:line:col:`) was added later but is backwards-compatible. Both formats coexist in modern mypy (1.x).
- **Alternatives considered**: Parsing mypy's `--output json` flag was considered but rejected -- it would require RTK to inject flags, which conflicts with the argument passthrough design (FR-010). The text format is sufficient and avoids modifying user intent.

### Command Discovery (mypy vs python -m mypy)

- **Decision**: Try `mypy` directly first, fall back to `python3 -m mypy`.
- **Rationale**: This is the same pattern used by `pytest_cmd.rs` (lines 18-25). Users who install mypy via pip have it on PATH. Users in virtualenvs or poetry/pipx may only have it via `python -m mypy`.
- **Alternatives considered**: Only supporting `mypy` directly was considered but would miss users who haven't activated their virtualenv.

### ANSI Stripping

- **Decision**: Reuse existing `utils::strip_ansi()` (src/utils.rs:47).
- **Rationale**: Mypy colorizes output when stdout is a TTY. Since RTK captures via `Command::output()` (not a TTY), mypy typically does not emit ANSI. However, users may have `MYPY_FORCE_COLOR=1` or `--color-output` set, so stripping is a safety measure.
- **Alternatives considered**: None -- the utility already exists.

### Error Code Format

- **Decision**: Display error codes in bracket format `[error-code]` matching mypy's native format.
- **Rationale**: Mypy uses bracketed codes like `[return-value]`, `[name-defined]`, `[assignment]`. This differs from tsc which uses `TS2322` style. Using brackets preserves the code as-is for easy copy-paste into mypy configuration (`# type: ignore[error-code]`).
- **Alternatives considered**: Stripping brackets was considered but reduces utility for suppression comments.

## No Unresolved Unknowns

All technical decisions are resolved. No NEEDS CLARIFICATION items remain.
Loading
Loading