feat: scaffold remove command foundations

This commit is contained in:
Rogee
2025-10-29 18:21:01 +08:00
parent ceea09f7be
commit 446bd46b95
25 changed files with 994 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ Auto-generated from all feature plans. Last updated: 2025-10-29
## Active Technologies
- Local filesystem (no persistent database) (002-add-replace-command)
- Go 1.24 + `spf13/cobra`, `spf13/pflag` (001-list-command-filters)
- Local filesystem only (ledger persisted as `.renamer`) (003-add-remove-command)
## Project Structure
@@ -36,6 +37,7 @@ tests/
- Smoke: `scripts/smoke-test-replace.sh`
## Recent Changes
- 003-add-remove-command: Added Go 1.24 + `spf13/cobra`, `spf13/pflag`
- 002-add-replace-command: Added `renamer replace` command, ledger metadata, and automation docs.
- 001-list-command-filters: Added `renamer list` command with shared scope flags and formatters.

View File

@@ -0,0 +1,8 @@
package remove
import "errors"
var (
// ErrNoTokens indicates that no removal tokens were provided.
ErrNoTokens = errors.New("at least one non-empty token is required")
)

34
internal/remove/parser.go Normal file
View File

@@ -0,0 +1,34 @@
package remove
import "strings"
// ParseArgsResult captures parser output for sequential removals.
type ParseArgsResult struct {
Tokens []string
Duplicates []string
}
// ParseArgs splits, trims, and deduplicates tokens while preserving order.
func ParseArgs(args []string) (ParseArgsResult, error) {
result := ParseArgsResult{Tokens: make([]string, 0, len(args))}
seen := make(map[string]int)
for _, raw := range args {
token := strings.TrimSpace(raw)
if token == "" {
continue
}
if _, exists := seen[token]; exists {
result.Duplicates = append(result.Duplicates, token)
continue
}
seen[token] = len(result.Tokens)
result.Tokens = append(result.Tokens, token)
}
if len(result.Tokens) == 0 {
return ParseArgsResult{}, ErrNoTokens
}
return result, nil
}

View File

@@ -0,0 +1,53 @@
package remove
import (
"fmt"
"github.com/rogeecn/renamer/internal/listing"
)
// Request encapsulates the options required for remove operations.
// It mirrors the listing scope so preview/apply flows stay consistent.
type Request struct {
WorkingDir string
Tokens []string
IncludeDirectories bool
Recursive bool
IncludeHidden bool
Extensions []string
}
// FromListing builds a Request from the shared listing scope plus ordered tokens.
func FromListing(scope *listing.ListingRequest, tokens []string) (*Request, error) {
if scope == nil {
return nil, fmt.Errorf("scope must not be nil")
}
req := &Request{
WorkingDir: scope.WorkingDir,
IncludeDirectories: scope.IncludeDirectories,
Recursive: scope.Recursive,
IncludeHidden: scope.IncludeHidden,
Extensions: append([]string(nil), scope.Extensions...),
Tokens: append([]string(nil), tokens...),
}
if err := req.Validate(); err != nil {
return nil, err
}
return req, nil
}
// Validate ensures the request has the required data before traversal happens.
func (r *Request) Validate() error {
if r.WorkingDir == "" {
return fmt.Errorf("working directory must be provided")
}
if len(r.Tokens) == 0 {
return fmt.Errorf("at least one removal token is required")
}
for i, token := range r.Tokens {
if token == "" {
return fmt.Errorf("token at position %d is empty after trimming", i)
}
}
return nil
}

113
internal/remove/summary.go Normal file
View File

@@ -0,0 +1,113 @@
package remove
import "sort"
// Summary aggregates results across preview/apply phases.
type Summary struct {
totalCandidates int
changedCount int
conflicts []Conflict
empties []string
tokenMatches map[string]int
duplicates []string
}
// Conflict describes a rename conflict detected during planning.
type Conflict struct {
Original string
Proposed string
Reason string
}
// NewSummary constructs a ready-to-use Summary.
func NewSummary() Summary {
return Summary{
tokenMatches: make(map[string]int),
conflicts: make([]Conflict, 0),
empties: make([]string, 0),
duplicates: make([]string, 0),
}
}
// RecordCandidate increments the total candidate count.
func (s *Summary) RecordCandidate() {
s.totalCandidates++
}
// RecordChange increments changed items.
func (s *Summary) RecordChange() {
s.changedCount++
}
// AddTokenMatch records the number of matches for a token.
func (s *Summary) AddTokenMatch(token string, count int) {
s.tokenMatches[token] += count
}
// AddConflict registers a detected conflict.
func (s *Summary) AddConflict(c Conflict) {
s.conflicts = append(s.conflicts, c)
}
// AddEmpty registers a path skipped due to empty result names.
func (s *Summary) AddEmpty(path string) {
s.empties = append(s.empties, path)
}
// AddDuplicate tracks duplicate tokens encountered during parsing.
func (s *Summary) AddDuplicate(token string) {
s.duplicates = append(s.duplicates, token)
}
// TotalCandidates returns how many items were considered.
func (s Summary) TotalCandidates() int {
return s.totalCandidates
}
// ChangedCount returns the number of items whose names changed.
func (s Summary) ChangedCount() int {
return s.changedCount
}
// Conflicts returns a copy of conflict info.
func (s Summary) Conflicts() []Conflict {
out := make([]Conflict, len(s.conflicts))
copy(out, s.conflicts)
return out
}
// Empties returns paths skipped for empty basename results.
func (s Summary) Empties() []string {
out := make([]string, len(s.empties))
copy(out, s.empties)
return out
}
// TokenMatches returns a sorted slice of tokens and counts.
func (s Summary) TokenMatches() []struct {
Token string
Count int
} {
pairs := make([]struct {
Token string
Count int
}, 0, len(s.tokenMatches))
for token, count := range s.tokenMatches {
pairs = append(pairs, struct {
Token string
Count int
}{Token: token, Count: count})
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Token < pairs[j].Token
})
return pairs
}
// Duplicates returns duplicates flagged by the parser.
func (s Summary) Duplicates() []string {
out := make([]string, len(s.duplicates))
copy(out, s.duplicates)
sort.Strings(out)
return out
}

View File

@@ -0,0 +1,73 @@
package remove
import (
"context"
"io/fs"
"path/filepath"
"strings"
"github.com/rogeecn/renamer/internal/traversal"
)
// Candidate represents a file or directory eligible for token removal.
type Candidate struct {
RelativePath string
OriginalPath string
BaseName string
IsDir bool
Depth int
}
// Traverse walks the working directory according to the request scope and invokes fn for each
// candidate (files by default, directories when IncludeDirectories is true).
func Traverse(ctx context.Context, req *Request, fn func(Candidate) error) error {
if err := req.Validate(); err != nil {
return err
}
extensions := make(map[string]struct{}, len(req.Extensions))
for _, ext := range req.Extensions {
lower := strings.ToLower(ext)
extensions[lower] = struct{}{}
}
walker := traversal.NewWalker()
return walker.Walk(
req.WorkingDir,
req.Recursive,
req.IncludeDirectories,
req.IncludeHidden,
0,
func(relPath string, entry fs.DirEntry, depth int) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
isDir := entry.IsDir()
ext := strings.ToLower(filepath.Ext(entry.Name()))
if !isDir && len(extensions) > 0 {
if _, ok := extensions[ext]; !ok {
return nil
}
}
candidate := Candidate{
RelativePath: filepath.ToSlash(relPath),
OriginalPath: filepath.Join(req.WorkingDir, relPath),
BaseName: entry.Name(),
IsDir: isDir,
Depth: depth,
}
if candidate.RelativePath == "." {
return nil
}
return fn(candidate)
},
)
}

57
scripts/smoke-test-remove.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="go run"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
mkdir -p "$TMP_DIR/nested"
touch "$TMP_DIR/report copy draft.txt"
touch "$TMP_DIR/nested/notes draft.txt"
echo "Previewing removals..."
$BIN "$ROOT_DIR/main.go" remove " copy" " draft" --path "$TMP_DIR" --recursive --dry-run >/dev/null
if [[ ! -f "$TMP_DIR/report copy draft.txt" ]]; then
echo "preview should not modify files" >&2
exit 1
fi
echo "Applying removals..."
$BIN "$ROOT_DIR/main.go" remove " copy" " draft" --path "$TMP_DIR" --recursive --yes >/dev/null
if [[ ! -f "$TMP_DIR/report.txt" ]]; then
echo "expected report.txt to exist after removal" >&2
exit 1
fi
if [[ ! -f "$TMP_DIR/nested/notes.txt" ]]; then
echo "expected nested/notes.txt to exist after removal" >&2
exit 1
fi
if [[ -f "$TMP_DIR/report copy draft.txt" ]]; then
echo "source file still exists after apply" >&2
exit 1
fi
echo "Undoing removals..."
$BIN "$ROOT_DIR/main.go" undo --path "$TMP_DIR" >/dev/null
if [[ ! -f "$TMP_DIR/report copy draft.txt" ]]; then
echo "undo failed to restore report copy draft.txt" >&2
exit 1
fi
if [[ ! -f "$TMP_DIR/nested/notes draft.txt" ]]; then
echo "undo failed to restore nested/notes draft.txt" >&2
exit 1
fi
if [[ -f "$TMP_DIR/report.txt" ]]; then
echo "undo failed to clean up report.txt" >&2
exit 1
fi
echo "Remove smoke test succeeded."

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: Remove Command with Sequential Multi-Pattern Support
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-10-29
**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
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@@ -0,0 +1,68 @@
# CLI Contract: `renamer remove`
## Command Synopsis
```bash
renamer remove <pattern1> [pattern2 ...] [flags]
```
### Global Flags (inherited from root command)
- `--path <dir>` (defaults to current working directory)
- `-r`, `--recursive`
- `-d`, `--include-dirs`
- `--hidden`
- `-e`, `--extensions <.ext|.ext2>`
- `--dry-run`
- `--yes`
## Description
Sequentially removes literal substrings from file and directory names. Every token is applied in the
order provided, and the resulting name is used for subsequent removals before any filesystem rename
occurs.
## Arguments & Flags
| Argument | Required | Description |
|----------|----------|-------------|
| `<pattern...>` | Yes (≥1) | Literal substrings to remove sequentially. Quotes required when tokens contain spaces. |
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--path` | string | `.` | Working directory for traversal (global flag). |
| `-r`, `--recursive` | bool | `false` | Traverse subdirectories (global flag). |
| `-d`, `--include-dirs` | bool | `false` | Include directory names in removal scope (global flag). |
| `--hidden` | bool | `false` | Include hidden files/directories (global flag). |
| `-e`, `--extensions` | string | (none) | Restrict removals to files matching `|`-delimited extensions (global flag). |
| `--dry-run` | bool | `false` | Preview only; print proposed removals without applying (global flag). |
| `--yes` | bool | `false` | Apply changes without interactive prompt (global flag). |
## Exit Codes
| Code | Meaning | Example Trigger |
|------|---------|-----------------|
| `0` | Success | Preview completed or apply executed without conflicts. |
| `2` | Validation error | No patterns provided, empty token after trimming, unreadable directory. |
| `3` | Conflict detected | Target filename already exists after removals. |
## Preview Output
- Lists each impacted item with columns: `PATH`, `TOKENS REMOVED`, `NEW PATH`.
- Summary line: `Total: <candidates> (changed: <count>, empties: <skipped>, conflicts: <count>)`.
## Apply Behavior
- Re-validates preview results, then performs renames in sequence while tracking undo metadata.
- Writes ledger entry containing ordered token list and per-token match counts.
- Aborts without partial renames if conflicts arise between preview and apply.
## Validation Rules
- Tokens are deduplicated case-sensitively but order of first occurrence preserved; duplicates logged
as warnings.
- Resulting names that collapse to empty strings are skipped with warnings.
- Conflicts (targets already existing) abort the operation; users must resolve manually.
## Examples
```bash
renamer remove " copy" " draft"
renamer remove foo foo- bar --path ./reports --recursive --dry-run
renamer remove "Project X" " X" --extensions .txt|.md --yes
```

View File

@@ -0,0 +1,47 @@
# Data Model: Remove Command with Sequential Multi-Pattern Support
## Entities
### RemoveRequest
- **Description**: Captures inputs driving a remove operation.
- **Fields**:
- `workingDir` (string): Absolute path where traversal begins.
- `tokens` ([]string): Ordered list of literal substrings to remove sequentially.
- `includeDirectories` (bool): Whether directory names participate.
- `recursive` (bool): Whether to traverse subdirectories.
- `includeHidden` (bool): Include hidden files/directories when true.
- `extensions` ([]string): Optional extension filters inherited from global scope flags.
- `dryRun` (bool): Preview flag; true during preview, false for apply.
- **Validations**:
- `tokens` MUST contain at least one non-empty string after trimming.
- `workingDir` MUST exist and be readable prior to traversal.
- `tokens` are deduplicated case-sensitively but order of first occurrence preserved.
### RemoveSummary
- **Description**: Aggregates preview/apply outcomes for reporting and ledger.
- **Fields**:
- `totalCandidates` (int): Count of files/directories evaluated.
- `changedCount` (int): Count of items whose names change after removals.
- `tokenMatches` (map[string]int): Number of occurrences removed per token (ordered in ledger metadata).
- `conflicts` ([]ConflictDetail): Detected rename conflicts preventing apply.
- `empties` ([]string): Relative paths where removal would lead to empty basename (skipped).
### ConflictDetail
- **Description**: Captures rename conflicts detected during preview.
- **Fields**:
- `originalPath` (string)
- `proposedPath` (string)
- `reason` (string)
## Relationships
- `RemoveRequest` feeds traversal utilities to produce candidate names.
- `RemoveSummary` aggregates results from sequential removal engine and is persisted to ledger entries.
- `ConflictDetail` entries inform preview output and determine which renames are skipped.
## State Transitions
1. **Parse**: CLI arguments parsed into `RemoveRequest`; validations ensure tokens and scope are valid.
2. **Preview**: Sequential removal engine produces proposed names, conflicts, and warnings recorded in
`RemoveSummary`.
3. **Apply**: Upon confirmation/`--yes`, renames execute (in dependency order), ledger entry written
with ordered token metadata.
4. **Undo**: Ledger reverse operation uses stored operations to restore original names.

View File

@@ -0,0 +1,94 @@
# Implementation Plan: Remove Command with Sequential Multi-Pattern Support
**Branch**: `003-add-remove-command` | **Date**: 2025-10-29 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/003-add-remove-command/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
Introduce a `renamer remove` subcommand that deletes multiple literal substrings sequentially while
respecting preview → confirm → ledger → undo guarantees. The command must evaluate all removals in
memory before touching the filesystem to avoid excessive IO and ensure conflict detection happens on
final names.
## Technical Context
**Language/Version**: Go 1.24
**Primary Dependencies**: `spf13/cobra`, `spf13/pflag`
**Storage**: Local filesystem only (ledger persisted as `.renamer`)
**Testing**: Go `testing` package with contract, integration, and unit tests
**Target Platform**: Cross-platform CLI (Linux, macOS, Windows)
**Project Type**: Single CLI project
**Performance Goals**: Preview + apply for 100 files completes within 2 minutes
**Constraints**: Deterministic previews, reversible ledger entries, conflict/empty-name detection before apply
**Scale/Scope**: Handles hundreds of files per invocation; token list expected to be small (≤20 literals)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Preview flow MUST show deterministic rename mappings and require explicit confirmation (Preview-First Safety).
- Undo strategy MUST describe how the `.renamer` ledger entry is written and reversed (Persistent Undo Ledger).
- Planned remove rules MUST document their inputs, validations, and composing order (Composable Rule Engine).
- Scope handling MUST cover files vs directories (`-d`), recursion (`-r`), and extension filtering via `-e` without escaping the requested path (Scope-Aware Traversal).
- CLI UX plan MUST confirm Cobra usage, flag naming, help text, and automated tests for preview/undo flows (Ergonomic CLI Stewardship).
**Gate Alignment**:
- Remove command will reuse preview/confirm pipeline; no rename occurs before preview approval.
- Ledger entries will include ordered tokens removed per file to maintain undo guarantees.
- Removal logic will be implemented as composable rule(s) similar to replace, enabling reuse and testing.
- Command will consume shared scope flags (`--path`, `-r`, `-d`, `--hidden`, `-e`, `--dry-run`, `--yes`).
- Cobra wiring + automated tests will cover help text, sequential behavior warnings, and undo parity.
## Project Structure
### Documentation (this feature)
```text
specs/003-add-remove-command/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
└── tasks.md
```
### Source Code (repository root)
```text
cmd/
├── root.go
├── list.go
├── replace.go
├── remove.go # new CLI command for sequential removal
└── undo.go
internal/
├── listing/
├── replace/
├── remove/ # new package mirroring replace for request/parser/engine/summary
├── output/
├── traversal/
└── history/
scripts/
├── smoke-test-list.sh
├── smoke-test-replace.sh
└── smoke-test-remove.sh # new end-to-end smoke test
tests/
├── contract/
├── integration/
├── unit/
└── fixtures/
```
**Structure Decision**: Single CLI repository with new remove-specific logic under `internal/remove/`
and CLI wiring in `cmd/remove.go`. Tests follow the existing contract/integration/unit layout and a
new smoke test script will live under `scripts/`.
## Complexity Tracking
No constitution gate violations identified; no additional complexity justification required.

View File

@@ -0,0 +1,57 @@
# Quickstart: Remove Command with Sequential Multi-Pattern Support
## Goal
Demonstrate how to delete multiple substrings sequentially from filenames while using the
preview → apply → undo workflow safely.
## Prerequisites
- Go toolchain (>= 1.24) installed for building the CLI locally.
- Sample directory containing files with recurring tokens (e.g., `draft`, `copy`).
## Steps
1. **Build the CLI**
```bash
go build -o renamer ./...
```
2. **Inspect remove help**
```bash
./renamer remove --help
```
Pay attention to sequential behavior: tokens execute in the order provided.
3. **Run a preview with multiple tokens**
```bash
./renamer remove " copy" " draft" --path ./samples --dry-run
```
Confirm the output table shows each token removed in order and the summary reflects changed files.
4. **Apply removals after review**
```bash
./renamer remove " copy" " draft" --path ./samples --yes
```
Verify filenames no longer contain the tokens and a ledger entry is created.
5. **Undo if necessary**
```bash
./renamer undo --path ./samples
```
Ensure filenames return to their original state.
6. **Handle empty-result warnings**
```bash
./renamer remove "project" "project-" --path ./samples --dry-run
```
Expect the preview to warn and skip items that would collapse to empty names.
7. **Integrate into automation**
```bash
./renamer remove foo bar --dry-run && \
./renamer remove foo bar --yes --path ./automation
```
Use non-zero exit codes to detect invalid input in scripts.
## Next Steps
- Add contract tests covering sequential removals and empty-name warnings.
- Extend documentation (CLI reference, changelog) with remove command examples.

View File

@@ -0,0 +1,37 @@
# Phase 0 Research: Remove Command with Sequential Multi-Pattern Support
## Decision: Sequential removal executed in-memory before filesystem writes
- **Rationale**: Computing the full rename plan in memory guarantees deterministic previews,
simplifies conflict detection, and avoids partial renames that could increase IO load or leave the
filesystem inconsistent.
- **Alternatives considered**:
- *Apply-and-check per token*: rejected due to repeated filesystem mutations and difficulty keeping
undo history coherent.
- *Streaming rename per file*: rejected because conflicts can only be detected after all tokens
apply.
## Decision: Dedicated `internal/remove` package mirroring replace architecture
- **Rationale**: Keeps responsibilities separated (parser, engine, summary) and allows reuse of
traversal/history helpers. Aligns with Composable Rule Engine principle.
- **Alternatives considered**:
- *Extending replace package*: rejected to avoid coupling distinct behaviors and tests.
- *Embedding logic directly in command*: rejected for testability and maintainability reasons.
## Decision: Empty-result handling warns and skips rename
- **Rationale**: Removing multiple tokens could produce empty basenames; skipping prevents creating
invalid filenames while still informing the user.
- **Alternatives considered**:
- *Allow empty names*: rejected as unsafe and difficult to undo cleanly on certain filesystems.
- *Hard fail entire batch*: rejected because unaffected files should still be processed.
## Decision: Ledger metadata records ordered tokens and counts
- **Rationale**: Automation and undo workflows need insight into which tokens were removed and how
often, mirroring replaces metadata for consistency.
- **Alternatives considered**:
- *Only store operations*: insufficient for auditing complex removals.
## Decision: CLI help & quickstart emphasize ordering semantics
- **Rationale**: Sequential behavior is the primary mental model difference from other commands; clear
documentation reduces support load and user confusion.
- **Alternatives considered**:
- *Rely on examples alone*: risk of users assuming parallel removal and encountering surprises.

View File

@@ -0,0 +1,140 @@
# Feature Specification: Remove Command with Sequential Multi-Pattern Support
**Feature Branch**: `003-add-remove-command`
**Created**: 2025-10-29
**Status**: Draft
**Input**: User description: "添加 移除Remove命令用于删除指定字符、字符串支持同时删除多个字符串示例renamer remove str1 str2 ....注意多个移除时后续参数的移除依赖于前一个移除后的结果移除计算完成前不进行重命名避免IO负载过高"
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Remove Unwanted Tokens in One Pass (Priority: P1)
As a CLI user tidying filenames, I want `renamer remove` to delete multiple substrings in order so I
can normalize file names without writing custom scripts.
**Why this priority**: Delivers the core value—batch cleanup of recurring tokens across many files
with predictable results.
**Independent Test**: Run `renamer remove " copy" " draft" --dry-run` in a sample directory,
confirm preview shows the ordered removal effects, then apply with `--yes` and verify names update
accordingly.
**Acceptance Scenarios**:
1. **Given** files `report copy draft.txt` and `notes draft.txt`, **When** the user runs
`renamer remove " copy" " draft"`, **Then** the preview shows both tokens removed sequentially
and execution produces `report.txt` and `notes.txt`.
2. **Given** patterns where later removals depend on earlier results (e.g., removing `foo` then `foo-`),
**When** the command runs, **Then** each removal applies to the output of the previous step before
computing rename conflicts.
---
### User Story 2 - Script-Friendly Removal Workflow (Priority: P2)
As an operator automating rename tasks, I want deterministic previews, exit codes, and ledger entries
so scripts can run `renamer remove` safely without interactive prompts.
**Why this priority**: Ensures automation pipelines can rely on the same safety guarantees as manual
runs.
**Independent Test**: In a CI script, call `renamer remove ... --dry-run`, assert exit code 0, then
run with `--yes` and verify ledger entry plus `renamer undo` restores originals.
**Acceptance Scenarios**:
1. **Given** a non-interactive context, **When** the user passes `--yes` after a successful preview,
**Then** the command exits 0 on success and writes a ledger entry capturing tokens removed per file.
2. **Given** invalid input (e.g., fewer than two arguments), **When** the command executes, **Then** it
exits with non-zero status and instructs the user on correct sequential argument usage.
---
### User Story 3 - Validate Sequential Removal Inputs (Priority: P3)
As a power user managing complex token lists, I want clear validation and guidance for spaces,
duplicate tokens, and results that could produce empty filenames so I can adjust before applying.
**Why this priority**: Prevents surprise failures when tokens overlap or yield empty names.
**Independent Test**: Run `renamer remove "Project X" " Project" "X" --dry-run`, confirm preview
shows the sequential impact and warns if names collapse; invalid quoting should produce actionable
errors.
**Acceptance Scenarios**:
1. **Given** duplicate tokens, **When** the command runs, **Then** duplicates are deduplicated with a
warning and order preserved for remaining unique tokens.
2. **Given** a removal sequence that would produce an empty basename, **When** the preview runs,
**Then** the command warns and excludes the rename unless the user overrides in a future version.
---
### Edge Cases
- Sequential removals should operate on the output of prior removals within the same filename.
- Removing tokens may collapse names to empty strings or leave trailing separators; preview must flag
these cases before apply.
- Resulting names may collide; conflicts must be reported before confirmation.
- Hidden files or directories may need inclusion depending on scope flags (`--hidden`).
- No matches should result in a friendly "No entries matched" message and exit code 0 without ledger
writes.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The CLI MUST expose `renamer remove <pattern1> [pattern2 ...]` where all tokens are
literal substrings removed sequentially; final name computation MUST complete before any renames
occur.
- **FR-002**: Preview → confirm workflow MUST mirror existing commands, listing original and proposed
names with highlighted removals.
- **FR-003**: Executions MUST append detailed entries to `.renamer` including original names, tokens
removed (with order), resulting names, and timestamps so undo remains possible.
- **FR-004**: Users MUST be able to undo the most recent remove batch via existing undo mechanics
without leaving orphaned files.
- **FR-005**: Command MUST respect global scope flags (`--path`, `--recursive`, `--include-dirs`,
`--hidden`, `--extensions`, `--dry-run`, `--yes`) identical to `list` / `replace` behavior.
- **FR-006**: Preview MUST evaluate all removals first, calculate conflicts, and only then apply
filesystem operations when confirmed, limiting IO load.
- **FR-007**: Command MUST warn (and skip) renames that would result in empty basenames unless a
future explicit override flag is provided.
- **FR-008**: Invalid invocations (fewer than two arguments, empty tokens after trimming) MUST fail
with exit code ≠0 and actionable usage guidance.
- **FR-009**: Help output MUST document sequential behavior, whitespace quoting, and interaction with
other scope flags.
### Key Entities
- **RemoveRequest**: Captures working directory, scope flags, ordered token list, and preview/apply
options.
- **RemoveSummary**: Aggregates per-token match counts, per-file outcomes, conflicts, and warnings for
preview and ledger output.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users complete a sequential removal across 100 files (preview + apply) in under 2
minutes end-to-end.
- **SC-002**: 95% of usability test participants correctly understand that removals execute in the
provided order after reading `renamer remove --help`.
- **SC-003**: Automated regression tests confirm remove + undo leave the filesystem unchanged in
100% of scripted scenarios.
- **SC-004**: Support requests related to manual substring cleanup drop by 35% within the first release
cycle after launch.
## Assumptions
- Removals are literal substring matches; regex or wildcard support is out of scope for this release.
- Default matching is case-sensitive; case-insensitive options can be considered later if needed.
- Delete operations target filenames (and directories when `-d/--include-dirs` is set), not file
contents.
- Existing traversal, conflict detection, and ledger infrastructure can be extended for the remove
command.
## Dependencies & Risks
- Requires new remove-specific packages analogous to replace to maintain modularity.
- Help/quickstart documentation must be updated to explain sequential removal behavior.
- Potential filename conflicts or empty results must be detected pre-apply to avoid data loss.

View File

@@ -0,0 +1,168 @@
# Tasks: Remove Command with Sequential Multi-Pattern Support
**Input**: Design documents from `/specs/003-add-remove-command/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Prepare shared fixtures and tooling used across all remove command user stories.
- [X] T001 Create remove command fixture directories (`basic/`, `conflicts/`, `empties/`) with placeholder files and README in `tests/fixtures/remove-samples/`.
- [X] T002 [P] Author baseline smoke script showing preview → apply → undo flow for remove command in `scripts/smoke-test-remove.sh`.
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core remove command structures required before any user story implementation.
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
- [X] T003 [P] Define `RemoveRequest` struct plus validation and scope adaptation helpers in `internal/remove/request.go`.
- [X] T004 [P] Implement argument parsing (trimming, minimum token count, parse result object) in `internal/remove/parser.go`.
- [X] T005 [P] Create remove summary types and aggregation helpers for counts/conflicts in `internal/remove/summary.go`.
- [X] T006 Build traversal adapter that reuses listing scope to enumerate candidates for removal in `internal/remove/traversal.go`.
**Checkpoint**: Foundation ready—user story implementation can now begin in parallel.
---
## Phase 3: User Story 1 - Remove Unwanted Tokens in One Pass (Priority: P1) 🎯 MVP
**Goal**: Deliver sequential substring removal with preview and apply, covering the core filename cleanup workflow.
**Independent Test**: Run `renamer remove " copy" " draft" --dry-run` against `tests/fixtures/remove-samples/basic`, verify preview ordering, then apply with `--yes` and confirm filesystem changes.
### Tests for User Story 1 ⚠️
- [ ] T007 [P] [US1] Add unit tests covering sequential token application and unchanged cases in `tests/unit/remove_engine_test.go`.
- [ ] T008 [P] [US1] Create contract test validating preview table output and dry-run messaging in `tests/contract/remove_command_preview_test.go`.
- [ ] T009 [P] [US1] Write integration test exercising preview → apply flow with multiple files in `tests/integration/remove_flow_test.go`.
### Implementation for User Story 1
- [ ] T010 [US1] Implement sequential removal engine producing planned operations in `internal/remove/engine.go`.
- [ ] T011 [US1] Build preview pipeline that aggregates summaries, detects conflicts, and streams output in `internal/remove/preview.go`.
- [ ] T012 [US1] Implement apply pipeline executing planned operations without ledger writes in `internal/remove/apply.go`.
- [ ] T013 [US1] Wire new Cobra command in `cmd/remove.go` (with registration in `cmd/root.go`) to drive preview/apply using shared scope flags.
**Checkpoint**: User Story 1 functional end-to-end with preview/apply validated by automated tests.
---
## Phase 4: User Story 2 - Script-Friendly Removal Workflow (Priority: P2)
**Goal**: Ensure automation can run `renamer remove` non-interactively with deterministic exit codes and ledger-backed undo.
**Independent Test**: Execute `renamer remove foo bar --dry-run` followed by `--yes` inside CI fixture, verify exit code 0 on success, ledger metadata persists tokens, and `renamer undo` restores originals.
### Tests for User Story 2 ⚠️
- [ ] T014 [P] [US2] Add contract test asserting ledger entries capture ordered tokens and match counts in `tests/contract/remove_command_ledger_test.go`.
- [ ] T015 [P] [US2] Add integration test covering `--yes` automation path and subsequent undo in `tests/integration/remove_undo_test.go`.
### Implementation for User Story 2
- [ ] T016 [US2] Extend apply pipeline to append ledger entries with ordered tokens and match counts in `internal/remove/apply.go`.
- [ ] T017 [US2] Update `cmd/remove.go` to support non-interactive `--yes` execution, emit automation-oriented messages, and propagate exit codes.
**Checkpoint**: User Story 2 complete—CLI safe for scripting with ledger + undo parity.
---
## Phase 5: User Story 3 - Validate Sequential Removal Inputs (Priority: P3)
**Goal**: Provide clear validation and warnings for duplicate tokens, empty results, and risky removals.
**Independent Test**: Run `renamer remove "Project X" " Project" "X" --dry-run` and confirm duplicate dedupe warnings plus empty-result skips appear in preview output.
### Tests for User Story 3 ⚠️
- [ ] T018 [P] [US3] Add parser validation tests for duplicate tokens and whitespace edge cases in `tests/unit/remove_parser_test.go`.
- [ ] T019 [P] [US3] Add integration test verifying empty-basename warnings and skips in `tests/integration/remove_validation_test.go`.
### Implementation for User Story 3
- [ ] T020 [US3] Implement duplicate token deduplication with ordered warning collection in `internal/remove/parser.go`.
- [ ] T021 [US3] Add empty-basename detection and warning tracking in `internal/remove/summary.go`.
- [ ] T022 [US3] Surface duplicate and empty warnings in CLI output handling within `cmd/remove.go`.
**Checkpoint**: All user stories deliver value; validations prevent risky rename plans.
---
## Final Phase: Polish & Cross-Cutting Concerns
**Purpose**: Documentation, tooling, and quality improvements spanning all user stories.
- [ ] T023 [P] Update remove command documentation and sequential behavior guidance in `docs/cli-flags.md`.
- [ ] T024 Record release notes for remove command launch in `docs/CHANGELOG.md`.
- [ ] T025 [P] Finalize `scripts/smoke-test-remove.sh` with assertions and integrate into CI instructions.
- [ ] T026 Add remove command walkthrough to project onboarding materials in `AGENTS.md`.
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies—start immediately.
- **Foundational (Phase 2)**: Depends on Setup completion—BLOCKS all user stories.
- **User Stories (Phase 35)**: Each depends on Foundational phase; implement in priority order (P1 → P2 → P3) or in parallel once shared blockers clear.
- **Polish (Final Phase)**: Depends on completion of targeted user stories.
### User Story Dependencies
- **US1 (P1)**: Requires Foundational tasks (T003T006).
- **US2 (P2)**: Requires US1 core command and apply pipeline (T010T013).
- **US3 (P3)**: Requires parser and summary scaffolding plus US1 preview pipeline (T004T013).
### Within Each User Story
- Tests (if included) MUST be authored before implementation tasks.
- Engine/traversal logic precedes CLI wiring for predictable integration.
- Command wiring completes only after engine/preview/apply logic is ready.
### Parallel Opportunities
- Setup tasks (T001T002) can run in parallel.
- Foundational tasks marked [P] (T003T005) may proceed concurrently after directory scaffolding.
- US1 test tasks (T007T009) can run in parallel once fixtures exist.
- US2 and US3 test tasks (T014T019) can execute concurrently after their respective foundations.
- Polish tasks marked [P] (T023, T025) can occur alongside documentation updates.
---
## Parallel Example: User Story 1
```bash
# Parallel test development for US1:
# - T007: tests/unit/remove_engine_test.go
# - T008: tests/contract/remove_command_preview_test.go
# - T009: tests/integration/remove_flow_test.go
#
# Once tests are in place, run them together:
go test ./tests/unit ./tests/contract ./tests/integration -run Remove
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1 (Setup) and Phase 2 (Foundational).
2. Finish Phase 3 (US1) delivering preview/apply with automated coverage.
3. Validate with `go test ./...` and smoke script before moving on.
### Incremental Delivery
1. Deliver US1 (core removal) → release MVP.
2. Add US2 (automation + ledger) → publish update.
3. Enhance with US3 (advanced validation) → finalize release notes.
### Parallel Team Strategy
- After Phase 2, one developer tackles US1 implementation while another starts US2 tests.
- US3 validation enhancements can begin once parser scaffolding lands, overlapping with documentation polish.
- Conclude with Polish phase tasks to align docs, smoke tests, and onboarding materials.

View File

@@ -0,0 +1,9 @@
# Remove Command Fixtures
Sample directory structures used by remove command tests. Keep filenames ASCII and small.
- `basic/` — General-purpose samples demonstrating sequential token removals.
- `conflicts/` — Files that collide after token removal to exercise conflict handling.
- `empties/` — Names that collapse to empty or near-empty results to validate warnings.
Add directories/files sparingly so tests stay fast and portable.

View File

View File

View File

View File

View File

View File

View File